From 0b4913c6e47b696f7b620d4fd4963426f13d127e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:09:26 +0100 Subject: [PATCH 01/71] build(deps): bump commons-io:commons-io from 2.19.0 to 2.20.0 (#4003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [commons-io:commons-io](https://github.com/apache/commons-io) from 2.19.0 to 2.20.0.
Changelog

Sourced from commons-io:commons-io's changelog.

Apache Commons IO 2.20.0 Release Notes

The Apache Commons IO team is pleased to announce the release of Apache Commons IO 2.20.0.

Introduction

The Apache Commons IO library contains utility classes, stream implementations, file filters, file comparators, endian transformation classes, and much more.

Version 2.19.1: Java 8 or later is required.

New features

o IO-875: Add org.apache.commons.io.file.CountingPathVisitor.accept(Path, BasicFileAttributes) #743. Thanks to Pierre Baumard, Gary Gregory. o Add org.apache.commons.io.Charsets.isAlias(Charset, String). Thanks to Gary Gregory. o Add org.apache.commons.io.Charsets.isUTF8(Charset). Thanks to Gary Gregory. o Add org.apache.commons.io.Charsets.toCharsetDefault(String, Charset). Thanks to Gary Gregory. o IO-279: Add Tailer ignoreTouch option #757. Thanks to Joerg Budischewski, Gary Gregory.

Fixed Bugs

o [javadoc] Rename parameter of ProxyOutputStream.write(int) #740. Thanks to Jesse Glick. o IO-875: CopyDirectoryVisitor ignores fileFilter #743. Thanks to Pierre Baumard, Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.getReader(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.AbstractRandomAccessFileOrigin.getReader(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.ByeArrayOrigin.getReader(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.InputStreamOrigin.getReader(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.getWriter(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.AbstractRandomAccessFileOrigin.getWriter(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o org.apache.commons.io.build.AbstractOrigin.OutputStreamOrigin.getWriter(Charset) now maps a null Charset to the default Charset. Thanks to Gary Gregory. o FileUtils.readLines(File, Charset) now maps a null Charset to the default Charset #744. Thanks to Ryan Kurtz, Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashCr" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.WindowsLineEndingInputStream, org.apache.commons.io.input.WindowsLineEndingInputStream] At WindowsLineEndingInputStream.java:[line 77]Another occurrence at WindowsLineEndingInputStream.java:[line 81] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashCr" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.WindowsLineEndingInputStream] At WindowsLineEndingInputStream.java:[line 112] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashLf" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.WindowsLineEndingInputStream] At WindowsLineEndingInputStream.java:[line 113] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashLf" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.UnixLineEndingInputStream] At UnixLineEndingInputStream.java:[line 75] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atEos" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.UnixLineEndingInputStream] At UnixLineEndingInputStream.java:[line 120] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashCr" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.UnixLineEndingInputStream] At UnixLineEndingInputStream.java:[line 124] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "atSlashLf" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.UnixLineEndingInputStream] At UnixLineEndingInputStream.java:[line 125] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "closed" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.ProxyInputStream] At ProxyInputStream.java:[line 233] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o Fix SpotBugs [ERROR] Medium: Shared primitive variable "propagateClose" in one thread may not yield the value of the most recent write from another thread [org.apache.commons.io.input.BoundedInputStream] At BoundedInputStream.java:[line 555] AT_STALE_THREAD_WRITE_OF_PRIMITIVE. Thanks to Gary Gregory. o QueueInputStream reads all but the first byte without waiting. #748. Thanks to maxxedev, Piotr P. Karwasz, Gary Gregory. o Javadoc fixes and improvements. Thanks to Gary Gregory. o Avoid NPE in org.apache.commons.io.filefilter.WildcardFilter.accept(File). Thanks to Gary Gregory. o IO-874: FileUtils.forceDelete can delete a broken symlink again #756. Thanks to Andy Russell, Joerg Budischewski. o Fix infinite loop in AbstractByteArrayOutputStream. #758. Thanks to Alex Benusovich.

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=commons-io:commons-io&package-manager=gradle&previous-version=2.19.0&new-version=2.20.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- app/core/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/build.gradle b/app/core/build.gradle index 745dbb87a..ca7a007b7 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -43,7 +43,7 @@ dependencies { implementation project(':common') implementation 'org.springframework.boot:spring-boot-starter-jetty' implementation 'com.posthog.java:posthog:1.2.0' - implementation 'commons-io:commons-io:2.19.0' + implementation 'commons-io:commons-io:2.20.0' implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation 'io.micrometer:micrometer-core:1.15.2' From ea9b27719f72f4cf1dac6f971f8b1b9fddf9135e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:10:26 +0100 Subject: [PATCH 02/71] build(deps): bump alpine from 3.22.0 to 3.22.1 (#4011) Bumps alpine from 3.22.0 to 3.22.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=alpine&package-manager=docker&previous-version=3.22.0&new-version=3.22.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 2 +- Dockerfile.fat | 2 +- Dockerfile.ultra-lite | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 61c1dcc77..fe427fea9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Main stage -FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 # Copy necessary files COPY scripts /scripts diff --git a/Dockerfile.fat b/Dockerfile.fat index cdf2ba514..87cb5121c 100644 --- a/Dockerfile.fat +++ b/Dockerfile.fat @@ -22,7 +22,7 @@ RUN DISABLE_ADDITIONAL_FEATURES=false \ ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube # Main stage -FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 # Copy necessary files COPY scripts /scripts diff --git a/Dockerfile.ultra-lite b/Dockerfile.ultra-lite index 1e6219a85..85a9ab0ca 100644 --- a/Dockerfile.ultra-lite +++ b/Dockerfile.ultra-lite @@ -1,5 +1,5 @@ # use alpine -FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be023728e11715 +FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 ARG VERSION_TAG From b1bbad53bc1e4bb56d4e48ab31994e3e2f0ad53f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:10:45 +0100 Subject: [PATCH 03/71] build(deps): bump step-security/harden-runner from 2.12.2 to 2.13.0 (#4007) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.12.2 to 2.13.0.
Release notes

Sourced from step-security/harden-runner's releases.

v2.13.0

What's Changed

  • Improved job markdown summary
  • Https monitoring for all domains (included with the enterprise tier)

Full Changelog: https://github.com/step-security/harden-runner/compare/v2...v2.13.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=step-security/harden-runner&package-manager=github_actions&previous-version=2.12.2&new-version=2.13.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/PR-Demo-Comment-with-react.yml | 4 ++-- .github/workflows/PR-Demo-cleanup.yml | 2 +- .github/workflows/ai_pr_title_review.yml | 2 +- .github/workflows/auto-labelerV2.yml | 2 +- .github/workflows/build.yml | 10 +++++----- .github/workflows/check_properties.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/licenses-update.yml | 2 +- .github/workflows/manage-label.yml | 2 +- .github/workflows/multiOSReleases.yml | 12 ++++++------ .github/workflows/pre_commit.yml | 2 +- .github/workflows/push-docker.yml | 2 +- .github/workflows/releaseArtifacts.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/sonarqube.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/swagger.yml | 2 +- .github/workflows/sync_files.yml | 2 +- .github/workflows/testdriver.yml | 6 +++--- 19 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 877a78524..013db2886 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -41,7 +41,7 @@ jobs: enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }} steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -152,7 +152,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/PR-Demo-cleanup.yml b/.github/workflows/PR-Demo-cleanup.yml index 855e804b2..29aea4389 100644 --- a/.github/workflows/PR-Demo-cleanup.yml +++ b/.github/workflows/PR-Demo-cleanup.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml index b9fd7c277..7c47b8d58 100644 --- a/.github/workflows/ai_pr_title_review.yml +++ b/.github/workflows/ai_pr_title_review.yml @@ -19,7 +19,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/auto-labelerV2.yml b/.github/workflows/auto-labelerV2.yml index bf290de76..bd998d197 100644 --- a/.github/workflows/auto-labelerV2.yml +++ b/.github/workflows/auto-labelerV2.yml @@ -13,7 +13,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d5b637899..cdca40e0b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,7 +44,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -117,7 +117,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -148,7 +148,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -194,7 +194,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -243,7 +243,7 @@ jobs: docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"] steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/check_properties.yml b/.github/workflows/check_properties.yml index da000201a..9fac8bde0 100644 --- a/.github/workflows/check_properties.yml +++ b/.github/workflows/check_properties.yml @@ -18,7 +18,7 @@ jobs: pull-requests: write # Allow writing to pull requests steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 154b6bdae..30c96a1b0 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/licenses-update.yml b/.github/workflows/licenses-update.yml index 23c15816f..dc6503c27 100644 --- a/.github/workflows/licenses-update.yml +++ b/.github/workflows/licenses-update.yml @@ -19,7 +19,7 @@ jobs: repository-projects: write # Required for enabling automerge steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/manage-label.yml b/.github/workflows/manage-label.yml index 15349a66d..1388ef0fb 100644 --- a/.github/workflows/manage-label.yml +++ b/.github/workflows/manage-label.yml @@ -15,7 +15,7 @@ jobs: issues: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 3cac33e1f..6f615417f 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -21,7 +21,7 @@ jobs: versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }} steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -60,7 +60,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -110,7 +110,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -148,7 +148,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -238,7 +238,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -301,7 +301,7 @@ jobs: contents: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index ba80e9bcd..c4697a965 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -16,7 +16,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index 432925f1a..c6f3b1c6b 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 701bb678e..85790f47b 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -23,7 +23,7 @@ jobs: version: ${{ steps.versionNumber.outputs.versionNumber }} steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -83,7 +83,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -161,7 +161,7 @@ jobs: file_suffix: "" steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 948a5a37b..eca90c9b8 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index f708a5b8d..b994d9338 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 237040f0a..88b150e29 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: pull-requests: write steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml index 463736b65..e038f699e 100644 --- a/.github/workflows/swagger.yml +++ b/.github/workflows/swagger.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/sync_files.yml b/.github/workflows/sync_files.yml index 620209dbb..dbcf7b1da 100644 --- a/.github/workflows/sync_files.yml +++ b/.github/workflows/sync_files.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 85c93a244..0143cea81 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -110,7 +110,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit @@ -144,7 +144,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2 + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 with: egress-policy: audit From 28e95438b3fecd422c6ba67351d2e2b1ecaef71a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:10:56 +0100 Subject: [PATCH 04/71] build(deps): bump github/codeql-action from 3.29.2 to 3.29.3 (#4008) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.2 to 3.29.3.
Release notes

Sourced from github/codeql-action's releases.

v3.29.3

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

3.29.3 - 21 Jul 2025

No user facing changes.

See the full CHANGELOG.md for more information.

Changelog

Sourced from github/codeql-action's changelog.

CodeQL Action Changelog

See the releases page for the relevant changes to the CodeQL CLI and language packs.

[UNRELEASED]

No user facing changes.

3.29.3 - 21 Jul 2025

No user facing changes.

3.29.2 - 30 Jun 2025

  • Experimental: When the quality-queries input for the init action is provided with an argument, separate .quality.sarif files are produced and uploaded for each language with the results of the specified queries. Do not use this in production as it is part of an internal experiment and subject to change at any time. #2935

3.29.1 - 27 Jun 2025

  • Fix bug in PR analysis where user-provided include query filter fails to exclude non-included queries. #2938
  • Update default CodeQL bundle version to 2.22.1. #2950

3.29.0 - 11 Jun 2025

  • Update default CodeQL bundle version to 2.22.0. #2925
  • Bump minimum CodeQL bundle version to 2.16.6. #2912

3.28.20 - 21 July 2025

3.28.19 - 03 Jun 2025

  • The CodeQL Action no longer includes its own copy of the extractor for the actions language, which is currently in public preview. The actions extractor has been included in the CodeQL CLI since v2.20.6. If your workflow has enabled the actions language and you have pinned your tools: property to a specific version of the CodeQL CLI earlier than v2.20.6, you will need to update to at least CodeQL v2.20.6 or disable actions analysis.
  • Update default CodeQL bundle version to 2.21.4. #2910

3.28.18 - 16 May 2025

  • Update default CodeQL bundle version to 2.21.3. #2893
  • Skip validating SARIF produced by CodeQL for improved performance. #2894
  • The number of threads and amount of RAM used by CodeQL can now be set via the CODEQL_THREADS and CODEQL_RAM runner environment variables. If set, these environment variables override the threads and ram inputs respectively. #2891

3.28.17 - 02 May 2025

  • Update default CodeQL bundle version to 2.21.2. #2872

3.28.16 - 23 Apr 2025

... (truncated)

Commits
  • d6bbdef Merge pull request #2977 from github/update-v3.29.3-7710ed11e
  • 210cc9b Update changelog for v3.29.3
  • 7710ed1 Merge pull request #2970 from github/cklin/diff-informed-feature-enable
  • 6a49a8c build: refresh js files
  • 3aef410 Add diff-informed-analysis-utils.test.ts
  • 614b64c Diff-informed analysis: disable for GHES below 3.19
  • aefb854 Feature.DiffInformedQueries: default to true
  • 03a2a17 Merge pull request #2967 from github/cklin/overlay-feature-flags
  • 07455ed Merge pull request #2972 from github/koesie10/ghes-satisfies
  • 3fb562d build: refresh js files
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=3.29.2&new-version=3.29.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index eca90c9b8..120a223ad 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -74,6 +74,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/upload-sarif@d6bbdef45e766d081b84a2def353b0055f728d3e # v3.29.3 with: sarif_file: results.sarif From c80aaf6cd2ec8f8d1bd5fde17146ef5740eb6afc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:11:10 +0100 Subject: [PATCH 05/71] build(deps): bump actions/checkout from 2.4.2 to 4.2.2 (#4010) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.2 to 4.2.2.
Release notes

Sourced from actions/checkout's releases.

v4.2.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v4.2.1...v4.2.2

v4.2.1

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.2.0...v4.2.1

v4.2.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.1.7...v4.2.0

v4.1.7

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v4.1.6...v4.1.7

v4.1.6

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v4.1.5...v4.1.6

v4.1.5

What's Changed

... (truncated)

Changelog

Sourced from actions/checkout's changelog.

Changelog

v4.2.2

v4.2.1

v4.2.0

v4.1.7

v4.1.6

v4.1.5

v4.1.4

v4.1.3

v4.1.2

v4.1.1

v4.1.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=2.4.2&new-version=4.2.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ludy --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cdca40e0b..c38571abb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: project: ${{ steps.changes.outputs.project }} openapi: ${{ steps.changes.outputs.openapi }} steps: - - uses: actions/checkout@7884fcad6b5d53d10323aee724dc68d8b9096a2e # v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Check for file changes uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 From d80c11dffa74c915c1ddb98fca4703b949366973 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:11:29 +0100 Subject: [PATCH 06/71] build(deps): bump sigstore/cosign-installer from 3.9.1 to 3.9.2 (#4009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [//]: # (dependabot-start) ⚠️ **Dependabot is rebasing this PR** ⚠️ Rebasing might not happen immediately, so don't worry if this takes some time. Note: if you make any changes to this PR yourself, they will take precedence over the rebase. --- [//]: # (dependabot-end) Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.9.1 to 3.9.2.
Release notes

Sourced from sigstore/cosign-installer's releases.

v3.9.2

What's Changed

Full Changelog: https://github.com/sigstore/cosign-installer/compare/v3.9.1...v3.9.2

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sigstore/cosign-installer&package-manager=github_actions&previous-version=3.9.1&new-version=3.9.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/multiOSReleases.yml | 2 +- .github/workflows/push-docker.yml | 2 +- .github/workflows/releaseArtifacts.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index 6f615417f..b55c7d402 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -252,7 +252,7 @@ jobs: - name: Install Cosign if: matrix.os == 'windows-latest' - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - name: Generate key pair if: matrix.os == 'windows-latest' diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index c6f3b1c6b..47cb40182 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -42,7 +42,7 @@ jobs: - name: Install cosign if: github.ref == 'refs/heads/master' - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 with: cosign-release: "v2.4.1" diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 85790f47b..ba970e885 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -95,7 +95,7 @@ jobs: run: ls -R - name: Install Cosign - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1 + uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2 - name: Generate key pair run: cosign generate-key-pair From b650d443a710ce5743d4450be3fcbf1229634a0d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:14:23 +0100 Subject: [PATCH 07/71] build(deps): bump springSecuritySamlVersion from 6.5.1 to 6.5.2 (#4020) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 0c62a0e07..d97911bbe 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ ext { imageioVersion = "3.12.0" lombokVersion = "1.18.38" bouncycastleVersion = "1.81" - springSecuritySamlVersion = "6.5.1" + springSecuritySamlVersion = "6.5.2" openSamlVersion = "4.3.2" commonmarkVersion = "0.25.0" googleJavaFormatVersion = "1.27.0" From c161000f85d5476406d068a5ef9244bbb7273dc7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:14:34 +0100 Subject: [PATCH 08/71] build(deps): bump com.diffplug.spotless from 7.1.0 to 7.2.1 (#4019) Bumps com.diffplug.spotless from 7.1.0 to 7.2.1. [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=com.diffplug.spotless&package-manager=gradle&previous-version=7.1.0&new-version=7.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d97911bbe..1e472e083 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" id "edu.sc.seis.launch4j" version "3.0.6" - id "com.diffplug.spotless" version "7.1.0" + id "com.diffplug.spotless" version "7.2.1" id "com.github.jk1.dependency-license-report" version "2.9" //id "nebula.lint" version "19.0.3" id "org.panteleyev.jpackageplugin" version "1.7.3" From 7d6b70871bad2a3ff810825f7382c49f55293943 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:53:21 +0100 Subject: [PATCH 09/71] url fixes for access issues (#4013) # Description of Changes This pull request introduces a new SSRF (Server-Side Request Forgery) protection mechanism for URL handling in the application. Key changes include adding a dedicated `SsrfProtectionService`, integrating SSRF-safe policies into HTML sanitization, and extending application settings to support configurable URL security options. ### SSRF Protection Implementation: * **`SsrfProtectionService`**: Added a new service to handle SSRF protection with configurable levels (`OFF`, `MEDIUM`, `MAX`) and checks for private networks, localhost, link-local addresses, and cloud metadata endpoints (`app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java`). ### Application Configuration Enhancements: * **`ApplicationProperties`**: Introduced a new `Html` configuration class with nested `UrlSecurity` settings, allowing fine-grained control over URL security, including allowed/blocked domains and internal TLDs (`app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java`). [[1]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R293) [[2]](diffhunk://#diff-1c357db0a3e88cf5bedd4a5852415fadad83b8b3b9eb56e67059d8b9d8b10702R346-R364) * **`settings.yml.template`**: Updated the configuration template to include the new `html.urlSecurity` settings, enabling users to customize SSRF protection behavior (`app/core/src/main/resources/settings.yml.template`). ### HTML Sanitization Updates: * **`CustomHtmlSanitizer`**: Integrated SSRF-safe URL validation into the HTML sanitizer by using the `SsrfProtectionService`. Added a custom policy for validating `img` tags' `src` attributes (`app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java`). --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: a Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../common/model/ApplicationProperties.java | 20 ++ .../common/service/SsrfProtectionService.java | 208 ++++++++++++++++++ .../common/util/CustomHtmlSanitizer.java | 60 ++++- .../software/common/util/EmlToPdf.java | 22 +- .../software/common/util/FileToPdf.java | 21 +- .../common/util/CustomHtmlSanitizerTest.java | 53 +++-- .../software/common/util/EmlToPdfTest.java | 63 ++++-- .../software/common/util/FileToPdfTest.java | 25 ++- .../api/converters/ConvertEmlToPDF.java | 6 +- .../api/converters/ConvertHtmlToPDF.java | 13 +- .../api/converters/ConvertMarkdownToPdf.java | 13 +- .../converters/ConvertOfficeController.java | 15 +- .../src/main/resources/settings.yml.template | 11 + testing/allEndpointsRemovedSettings.yml | 16 +- 14 files changed, 462 insertions(+), 84 deletions(-) create mode 100644 app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index e4edf2baa..91b328759 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -290,6 +290,7 @@ public class ApplicationProperties { private Datasource datasource; private Boolean disableSanitize; private Boolean enableUrlToPDF; + private Html html = new Html(); private CustomPaths customPaths = new CustomPaths(); private String fileUploadLimit; private TempFileManagement tempFileManagement = new TempFileManagement(); @@ -342,6 +343,25 @@ public class ApplicationProperties { } } + @Data + public static class Html { + private UrlSecurity urlSecurity = new UrlSecurity(); + + @Data + public static class UrlSecurity { + private boolean enabled = true; + private String level = "MEDIUM"; // MAX, MEDIUM, OFF + private List allowedDomains = new ArrayList<>(); + private List blockedDomains = new ArrayList<>(); + private List internalTlds = + Arrays.asList(".local", ".internal", ".corp", ".home"); + private boolean blockPrivateNetworks = true; + private boolean blockLocalhost = true; + private boolean blockLinkLocal = true; + private boolean blockCloudMetadata = true; + } + } + @Data public static class Datasource { private boolean enableCustomDatabase; diff --git a/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java b/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java new file mode 100644 index 000000000..97c2da12e --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/service/SsrfProtectionService.java @@ -0,0 +1,208 @@ +package stirling.software.common.service; + +import java.net.InetAddress; +import java.net.URI; +import java.net.UnknownHostException; +import java.util.regex.Pattern; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import stirling.software.common.model.ApplicationProperties; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SsrfProtectionService { + + private final ApplicationProperties applicationProperties; + + private static final Pattern DATA_URL_PATTERN = + Pattern.compile("^data:.*", Pattern.CASE_INSENSITIVE); + private static final Pattern FRAGMENT_PATTERN = Pattern.compile("^#.*"); + + public enum SsrfProtectionLevel { + OFF, // No SSRF protection - allows all URLs + MEDIUM, // Block internal networks but allow external URLs + MAX // Block all external URLs - only data: and fragments + } + + public boolean isUrlAllowed(String url) { + ApplicationProperties.Html.UrlSecurity config = + applicationProperties.getSystem().getHtml().getUrlSecurity(); + + if (!config.isEnabled()) { + return true; + } + + if (url == null || url.trim().isEmpty()) { + return false; + } + + String trimmedUrl = url.trim(); + + // Always allow data URLs and fragments + if (DATA_URL_PATTERN.matcher(trimmedUrl).matches() + || FRAGMENT_PATTERN.matcher(trimmedUrl).matches()) { + return true; + } + + SsrfProtectionLevel level = parseProtectionLevel(config.getLevel()); + + switch (level) { + case OFF: + return true; + case MAX: + return isMaxSecurityAllowed(trimmedUrl, config); + case MEDIUM: + return isMediumSecurityAllowed(trimmedUrl, config); + default: + return false; + } + } + + private SsrfProtectionLevel parseProtectionLevel(String level) { + try { + return SsrfProtectionLevel.valueOf(level.toUpperCase()); + } catch (IllegalArgumentException e) { + log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level); + return SsrfProtectionLevel.MEDIUM; + } + } + + private boolean isMaxSecurityAllowed( + String url, ApplicationProperties.Html.UrlSecurity config) { + // MAX security: only allow explicitly whitelisted domains + try { + URI uri = new URI(url); + String host = uri.getHost(); + + if (host == null) { + return false; + } + + return config.getAllowedDomains().contains(host.toLowerCase()); + + } catch (Exception e) { + log.debug("Failed to parse URL for MAX security check: {}", url, e); + return false; + } + } + + private boolean isMediumSecurityAllowed( + String url, ApplicationProperties.Html.UrlSecurity config) { + try { + URI uri = new URI(url); + String host = uri.getHost(); + + if (host == null) { + return false; + } + + String hostLower = host.toLowerCase(); + + // Check explicit blocked domains + if (config.getBlockedDomains().contains(hostLower)) { + log.debug("URL blocked by explicit domain blocklist: {}", url); + return false; + } + + // Check internal TLD patterns + for (String tld : config.getInternalTlds()) { + if (hostLower.endsWith(tld.toLowerCase())) { + log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url); + return false; + } + } + + // If allowedDomains is specified, only allow those + if (!config.getAllowedDomains().isEmpty()) { + boolean isAllowed = + config.getAllowedDomains().stream() + .anyMatch( + domain -> + hostLower.equals(domain.toLowerCase()) + || hostLower.endsWith( + "." + domain.toLowerCase())); + + if (!isAllowed) { + log.debug("URL not in allowed domains list: {}", url); + return false; + } + } + + // Resolve hostname to IP address for network-based checks + try { + InetAddress address = InetAddress.getByName(host); + + if (config.isBlockPrivateNetworks() && isPrivateAddress(address)) { + log.debug("URL blocked - private network address: {}", url); + return false; + } + + if (config.isBlockLocalhost() && address.isLoopbackAddress()) { + log.debug("URL blocked - localhost address: {}", url); + return false; + } + + if (config.isBlockLinkLocal() && address.isLinkLocalAddress()) { + log.debug("URL blocked - link-local address: {}", url); + return false; + } + + if (config.isBlockCloudMetadata() + && isCloudMetadataAddress(address.getHostAddress())) { + log.debug("URL blocked - cloud metadata endpoint: {}", url); + return false; + } + + } catch (UnknownHostException e) { + log.debug("Failed to resolve hostname for SSRF check: {}", host, e); + return false; + } + + return true; + + } catch (Exception e) { + log.debug("Failed to parse URL for MEDIUM security check: {}", url, e); + return false; + } + } + + private boolean isPrivateAddress(InetAddress address) { + return address.isSiteLocalAddress() + || address.isAnyLocalAddress() + || isPrivateIPv4Range(address.getHostAddress()); + } + + private boolean isPrivateIPv4Range(String ip) { + return ip.startsWith("10.") + || ip.startsWith("192.168.") + || (ip.startsWith("172.") && isInRange172(ip)) + || ip.startsWith("127.") + || "0.0.0.0".equals(ip); + } + + private boolean isInRange172(String ip) { + String[] parts = ip.split("\\."); + if (parts.length >= 2) { + try { + int secondOctet = Integer.parseInt(parts[1]); + return secondOctet >= 16 && secondOctet <= 31; + } catch (NumberFormatException e) { + return false; + } + } + return false; + } + + private boolean isCloudMetadataAddress(String ip) { + // Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud + return ip.startsWith("169.254.169.254") // AWS/GCP/Azure + || ip.startsWith("fd00:ec2::254") // AWS IPv6 + || ip.startsWith("169.254.169.253") // Oracle Cloud + || ip.startsWith("169.254.169.250"); // IBM Cloud + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java index e5fe0436a..05d9b73a6 100644 --- a/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java +++ b/app/common/src/main/java/stirling/software/common/util/CustomHtmlSanitizer.java @@ -1,21 +1,71 @@ package stirling.software.common.util; +import org.owasp.html.AttributePolicy; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.service.SsrfProtectionService; + +@Component public class CustomHtmlSanitizer { - private static final PolicyFactory POLICY = + + private final SsrfProtectionService ssrfProtectionService; + private final ApplicationProperties applicationProperties; + + @Autowired + public CustomHtmlSanitizer( + SsrfProtectionService ssrfProtectionService, + ApplicationProperties applicationProperties) { + this.ssrfProtectionService = ssrfProtectionService; + this.applicationProperties = applicationProperties; + } + + private final AttributePolicy SSRF_SAFE_URL_POLICY = + new AttributePolicy() { + @Override + public String apply(String elementName, String attributeName, String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + + String trimmedValue = value.trim(); + + // Use the SSRF protection service to validate the URL + if (ssrfProtectionService != null + && !ssrfProtectionService.isUrlAllowed(trimmedValue)) { + return null; + } + + return trimmedValue; + } + }; + + private final PolicyFactory SSRF_SAFE_IMAGES_POLICY = + new HtmlPolicyBuilder() + .allowElements("img") + .allowAttributes("alt", "width", "height", "title") + .onElements("img") + .allowAttributes("src") + .matching(SSRF_SAFE_URL_POLICY) + .onElements("img") + .toFactory(); + + private final PolicyFactory POLICY = Sanitizers.FORMATTING .and(Sanitizers.BLOCKS) .and(Sanitizers.STYLES) .and(Sanitizers.LINKS) .and(Sanitizers.TABLES) - .and(Sanitizers.IMAGES) + .and(SSRF_SAFE_IMAGES_POLICY) .and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory()); - public static String sanitize(String html) { - String htmlAfter = POLICY.sanitize(html); - return htmlAfter; + public String sanitize(String html) { + boolean disableSanitize = + Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize()); + return disableSanitize ? html : POLICY.sanitize(html); } } diff --git a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java index 05e9cec5c..6b28dc683 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java @@ -133,9 +133,9 @@ public class EmlToPdf { EmlToPdfRequest request, byte[] emlBytes, String fileName, - boolean disableSanitize, stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { validateEmlInput(emlBytes); @@ -155,7 +155,11 @@ public class EmlToPdf { // Convert HTML to PDF byte[] pdfBytes = convertHtmlToPdf( - weasyprintPath, request, htmlContent, disableSanitize, tempFileManager); + weasyprintPath, + request, + htmlContent, + tempFileManager, + customHtmlSanitizer); // Attach files if available and requested if (shouldAttachFiles(emailContent, request)) { @@ -196,8 +200,8 @@ public class EmlToPdf { String weasyprintPath, EmlToPdfRequest request, String htmlContent, - boolean disableSanitize, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { HTMLToPdfRequest htmlRequest = createHtmlRequest(request); @@ -208,8 +212,8 @@ public class EmlToPdf { htmlRequest, htmlContent.getBytes(StandardCharsets.UTF_8), "email.html", - disableSanitize, - tempFileManager); + tempFileManager, + customHtmlSanitizer); } catch (IOException | InterruptedException e) { log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML"); String simplifiedHtml = simplifyHtmlContent(htmlContent); @@ -218,8 +222,8 @@ public class EmlToPdf { htmlRequest, simplifiedHtml.getBytes(StandardCharsets.UTF_8), "email.html", - disableSanitize, - tempFileManager); + tempFileManager, + customHtmlSanitizer); } } diff --git a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java index c735e5287..799f91e05 100644 --- a/app/common/src/main/java/stirling/software/common/util/FileToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/FileToPdf.java @@ -26,8 +26,8 @@ public class FileToPdf { HTMLToPdfRequest request, byte[] fileBytes, String fileName, - boolean disableSanitize, - TempFileManager tempFileManager) + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) { @@ -39,14 +39,15 @@ public class FileToPdf { if (fileName.toLowerCase().endsWith(".html")) { String sanitizedHtml = sanitizeHtmlContent( - new String(fileBytes, StandardCharsets.UTF_8), disableSanitize); + new String(fileBytes, StandardCharsets.UTF_8), + customHtmlSanitizer); Files.write( tempInputFile.getPath(), sanitizedHtml.getBytes(StandardCharsets.UTF_8)); } else if (fileName.toLowerCase().endsWith(".zip")) { Files.write(tempInputFile.getPath(), fileBytes); sanitizeHtmlFilesInZip( - tempInputFile.getPath(), disableSanitize, tempFileManager); + tempInputFile.getPath(), tempFileManager, customHtmlSanitizer); } else { throw ExceptionUtils.createHtmlFileRequiredException(); } @@ -78,12 +79,15 @@ public class FileToPdf { } // tempOutputFile auto-closed } - private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) { - return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent; + private static String sanitizeHtmlContent( + String htmlContent, CustomHtmlSanitizer customHtmlSanitizer) { + return customHtmlSanitizer.sanitize(htmlContent); } private static void sanitizeHtmlFilesInZip( - Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager) + Path zipFilePath, + TempFileManager tempFileManager, + CustomHtmlSanitizer customHtmlSanitizer) throws IOException { try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) { try (ZipInputStream zipIn = @@ -99,7 +103,8 @@ public class FileToPdf { || entry.getName().toLowerCase().endsWith(".htm")) { String content = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8); - String sanitizedContent = sanitizeHtmlContent(content, disableSanitize); + String sanitizedContent = + sanitizeHtmlContent(content, customHtmlSanitizer); Files.write( filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8)); } else { diff --git a/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java b/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java index 65bffe05e..59e5f81b1 100644 --- a/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java +++ b/app/common/src/test/java/stirling/software/common/util/CustomHtmlSanitizerTest.java @@ -3,21 +3,42 @@ package stirling.software.common.util; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import stirling.software.common.service.SsrfProtectionService; + class CustomHtmlSanitizerTest { + private CustomHtmlSanitizer customHtmlSanitizer; + + @BeforeEach + void setUp() { + SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class); + stirling.software.common.model.ApplicationProperties mockApplicationProperties = mock(stirling.software.common.model.ApplicationProperties.class); + stirling.software.common.model.ApplicationProperties.System mockSystem = mock(stirling.software.common.model.ApplicationProperties.System.class); + + // Allow all URLs by default for basic tests + when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString())).thenReturn(true); + when(mockApplicationProperties.getSystem()).thenReturn(mockSystem); + when(mockSystem.getDisableSanitize()).thenReturn(false); // Enable sanitization for tests + + customHtmlSanitizer = new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties); + } + @ParameterizedTest @MethodSource("provideHtmlTestCases") void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) { // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(inputHtml); + String sanitizedHtml = customHtmlSanitizer.sanitize(inputHtml); // Assert for (String tag : expectedContainedTags) { @@ -58,7 +79,7 @@ class CustomHtmlSanitizerTest { "

Styled text

"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithStyles); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithStyles); // Assert // The OWASP HTML Sanitizer might filter some specific styles, so we only check that @@ -75,7 +96,7 @@ class CustomHtmlSanitizerTest { "
Example Link"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithLink); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithLink); // Assert // The most important aspect is that the link content is preserved @@ -97,7 +118,7 @@ class CustomHtmlSanitizerTest { String htmlWithJsLink = "Malicious Link"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsLink); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithJsLink); // Assert assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed"); @@ -116,7 +137,7 @@ class CustomHtmlSanitizerTest { + ""; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithTable); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithTable); // Assert assertTrue(sanitizedHtml.contains(""; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithImage); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithImage); // Assert assertTrue(sanitizedHtml.contains(""; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithDataUrlImage); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage); // Assert assertFalse( @@ -175,7 +196,7 @@ class CustomHtmlSanitizerTest { "Click me"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsEvent); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithJsEvent); // Assert assertFalse( @@ -192,7 +213,7 @@ class CustomHtmlSanitizerTest { String htmlWithScript = "

Safe content

"; // Act - String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithScript); + String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithScript); // Assert assertFalse(sanitizedHtml.contains(" diff --git a/devGuide/DeveloperGuide.md b/devGuide/DeveloperGuide.md index c37be9b84..fb8911eaf 100644 --- a/devGuide/DeveloperGuide.md +++ b/devGuide/DeveloperGuide.md @@ -295,6 +295,7 @@ Stirling-PDF can be customized through environment variables or a `settings.yml` - Security settings - UI customization - Endpoint management +- Maximum DPI for PDF to image conversion (`system.maxDPI`) When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file. From 6cd64a22ba42f42d19e34c32f0dca0ce0ada9dba Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 11:36:30 +0200 Subject: [PATCH 46/71] build(local): simplify writeVersion task with WriteProperties plugin and enable build caching (#4139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What was changed**: - Replaced the custom `writeVersion` task in `build.gradle` with the built-in `WriteProperties` plugin configuration. - Updated `gradle.properties` to enable `org.gradle.caching` (uncommented) for local development. - **Why the change was made**: - To reduce boilerplate and leverage Gradle’s native property-writing capabilities for maintaining the version file. - To improve build performance by reusing outputs via the Gradle build cache. - **Scope**: - These updates only affect local development and do not change production or CI script --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- build.gradle | 27 +++++---------------------- gradle.properties | 3 ++- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 627d7b5c1..fd9abf7c8 100644 --- a/build.gradle +++ b/build.gradle @@ -65,28 +65,11 @@ allprojects { } } -tasks.register('writeVersion') { - def propsFile = file("$projectDir/app/common/src/main/resources/version.properties") - def propsDir = propsFile.parentFile - - doLast { - if (propsDir.exists()) { - if (propsFile.exists()) { - println "File exists: $propsFile" - } else { - println "$propsFile does not exist. Creating file." - propsFile.createNewFile() - } - } else { - println "Creating directory: $propsDir" - propsDir.mkdirs() - propsFile.createNewFile() - } - - def props = new Properties() - props.setProperty("version", version) - props.store(propsFile.newWriter(), null) - } +tasks.register('writeVersion', WriteProperties) { + outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties') + println "Writing version.properties to ${outputFile.path}" + comment "${new Date()}" + property 'version', project.provider { project.version.toString() } } tasks.named('createExe') { diff --git a/gradle.properties b/gradle.properties index 9184cf5c6..8a390f592 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ org.gradle.parallel=true # Enables build caching to reuse outputs from previous builds for faster execution -# org.gradle.caching=true +org.gradle.caching=true org.gradle.build-scan=true +# org.gradle.configuration-cache=true From c4c9f3f3032c1259d33380c427131db4dc44ab5b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:38:57 +0100 Subject: [PATCH 47/71] Update CODEOWNERS (#4142) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/CODEOWNERS | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8d4e98e5a..7d5389fda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,21 @@ -# All PRs to V1 must be approved by Frooodle -* @Frooodle @reecebrowne @Ludy87 @DarioGii @ConnorYoh @EthanHealy01 +# All PRs must be approved by Frooodle or Ludy87 +* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh + +# Backend +/app/** @DarioGii + +#V1 frontend +/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 + +#V2 frontend +/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 + +#V2 docker +/docker/backend/** @Frooodle @Ludy87 @DarioGii +/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 + + +#GHA (All users) +/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 From d3c786d018293021f483cdfb8afcef62070335d4 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:21:29 +0100 Subject: [PATCH 48/71] :globe_with_meridians: Sync Translations + Update README Progress Table (#4135) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0a563fa5..b9660ce43 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Stirling-PDF currently supports 40 languages! | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) | -| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) | +| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) | | Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) | | Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | From b77d02e9884376d8e44a8e5cfa4a3193b9677bda Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 13:30:30 +0200 Subject: [PATCH 49/71] chore(templates): remove redundant `fetch-utils.js` script includes (#4092) # Description of Changes - **What was changed**: Removed all explicit `` tags from various Thymeleaf templates (`home.html`, `home-legacy.html`, `scanner-effect.html`, etc.). - **Why the change was made**: The `fetch-utils.js` script is already included globally via `` in `fragments/common.html` (line 156). Keeping redundant includes leads to unnecessary script loading and potential duplication. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- app/core/src/main/resources/templates/home-legacy.html | 3 --- app/core/src/main/resources/templates/home.html | 1 - app/core/src/main/resources/templates/misc/scanner-effect.html | 1 - .../src/main/resources/templates/misc/show-javascript.html | 1 - app/core/src/main/resources/templates/pipeline.html | 1 - .../src/main/resources/templates/security/get-info-on-pdf.html | 1 - 6 files changed, 8 deletions(-) diff --git a/app/core/src/main/resources/templates/home-legacy.html b/app/core/src/main/resources/templates/home-legacy.html index 9531a359b..3c01bcbd6 100644 --- a/app/core/src/main/resources/templates/home-legacy.html +++ b/app/core/src/main/resources/templates/home-legacy.html @@ -413,9 +413,6 @@ - - - diff --git a/app/core/src/main/resources/templates/security/get-info-on-pdf.html b/app/core/src/main/resources/templates/security/get-info-on-pdf.html index 86e65cd01..0b64bb679 100644 --- a/app/core/src/main/resources/templates/security/get-info-on-pdf.html +++ b/app/core/src/main/resources/templates/security/get-info-on-pdf.html @@ -106,7 +106,6 @@ - - + - + \ No newline at end of file From b91bfac41667c09bdd0979f07ca15eba9b025a38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:50:21 +0100 Subject: [PATCH 51/71] build(deps): bump docker/login-action from 3.4.0 to 3.5.0 (#4118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [docker/login-action](https://github.com/docker/login-action) from 3.4.0 to 3.5.0.
Release notes

Sourced from docker/login-action's releases.

v3.5.0

Full Changelog: https://github.com/docker/login-action/compare/v3.4.0...v3.5.0

Commits
  • 184bdaa Merge pull request #878 from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...
  • 5c6bc94 chore: update generated content
  • caf4058 build(deps): bump the aws-sdk-dependencies group with 2 updates
  • ef38ec3 Merge pull request #860 from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...
  • d52e8ef chore: update generated content
  • 9644ab7 build(deps): bump the aws-sdk-dependencies group with 2 updates
  • 7abd1d5 Merge pull request #875 from docker/dependabot/npm_and_yarn/form-data-2.5.5
  • 1a81202 Merge pull request #876 from crazy-max/aws-public-dual-stack
  • d1ab30d chore: update generated content
  • f25ff28 support dual-stack for aws public ecr
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=3.4.0&new-version=3.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/PR-Demo-Comment-with-react.yml | 2 +- .github/workflows/push-docker.yml | 4 ++-- .github/workflows/testdriver.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 013db2886..066d85ef2 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -196,7 +196,7 @@ jobs: uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index dbbc2622d..2a04ba33e 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -67,13 +67,13 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index cdb8b345d..b5759ed54 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -57,7 +57,7 @@ jobs: echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} From bb8edffaabc4196cfc300bbbd0d849eb2ac7e3cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:50:30 +0100 Subject: [PATCH 52/71] build(deps): bump actions/ai-inference from 1.2.3 to 1.2.4 (#4119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/ai-inference](https://github.com/actions/ai-inference) from 1.2.3 to 1.2.4.
Release notes

Sourced from actions/ai-inference's releases.

v1.2.4

What's Changed

Full Changelog: https://github.com/actions/ai-inference/compare/v1...v1.2.4

Commits
  • 4b591cc Merge pull request #83 from actions/sgoedecke/separate-mcp
  • ea24ec2 Update README.md
  • b9f9444 update docs
  • 419f171 Separate out MCP token
  • fc8527d Merge pull request #74 from actions/dependabot/github_actions/actions-minor-e...
  • 719349d Merge branch 'main' into dependabot/github_actions/actions-minor-e893b3f303
  • 2762750 Merge pull request #76 from actions/dependabot/npm_and_yarn/rollup/rollup-lin...
  • 9386906 chore(deps): bump @​rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
  • ca9eff7 chore(deps): bump actions/publish-action in the actions-minor group
  • 6bef1d0 Merge pull request #72 from actions/mr/linters
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/ai-inference&package-manager=github_actions&previous-version=1.2.3&new-version=1.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ai_pr_title_review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml index b7d944c34..8a2e8b8ef 100644 --- a/.github/workflows/ai_pr_title_review.yml +++ b/.github/workflows/ai_pr_title_review.yml @@ -87,7 +87,7 @@ jobs: - name: AI PR Title Analysis if: steps.actor.outputs.is_repo_dev == 'true' id: ai-title-analysis - uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3 + uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7 with: model: openai/gpt-4o system-prompt-file: ".github/config/system-prompt.txt" From b6ff1dd7f60f0b98ec5235e41775c38d67b99222 Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 13:52:51 +0200 Subject: [PATCH 53/71] chore: update development configs, formatting tools, and CI enhancements (#4130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What was changed** - Bumped `java.format.settings.google.version` to **1.28.0** in `.devcontainer/devcontainer.json` and `.vscode/settings.json`. - Expanded ignore patterns in `.devcontainer/devcontainer.json` to cover `app/core/`, `app/common/`, `app/proprietary/` directories. - Added a new top‐level `.dockerignore` to exclude build artifacts, virtual environments, logs, OS files, and markdown docs. - Consolidated EditorConfig YAML globs into `*.{yml,yaml}` to remove duplication. - Fixed missing newline in `.github/config/.files.yaml` and added label metadata (`from_name`/`description`) in `.github/labels.yml`. - Updated `build.gradle`: - Introduced `junitPlatformVersion = "1.12.2"` and replaced hard-coded launcher versions. - Applied the `jacoco` plugin across all subprojects and configured `jacocoTestReport` (XML + HTML). - Wire-up `jacocoTestReport` to run after tests. - **Why the change was made** - Ensure all formatting tools (Google Java Format) stay in sync across editors and containers. - Clean up ignore rules to prevent build artifacts and sensitive files from creeping into images and repos. - Improve CI visibility by generating code-coverage reports with JaCoCo. - Keep GitHub configuration files well-formed and enrich label definitions for automation. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .devcontainer/devcontainer.json | 13 ++++++++++++- .editorconfig | 8 +------- .github/config/.files.yaml | 2 +- .github/labels.yml | 5 +++++ .vscode/settings.json | 3 ++- build.gradle | 16 ++++++++++++++-- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5ab9f82c9..dcc0ca600 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -49,7 +49,7 @@ "java.configuration.updateBuildConfiguration": "interactive", "java.format.enabled": true, "java.format.settings.profile": "GoogleStyle", - "java.format.settings.google.version": "1.26.0", + "java.format.settings.google.version": "1.28.0", "java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting", "java.saveActions.cleanup": true, "java.cleanup.actions": [ @@ -79,9 +79,17 @@ ".venv*/", ".vscode/", "bin/", + "app/core/bin/", + "app/common/bin/", + "app/proprietary/bin/", "build/", + "app/core/build/", + "app/common/build/", + "app/proprietary/build/", "configs/", + "app/core/configs/", "customFiles/", + "app/core/customFiles/", "docs/", "exampleYmlFiles", "gradle/", @@ -93,6 +101,9 @@ ".git-blame-ignore-revs", ".gitattributes", ".gitignore", + "app/core/.gitignore", + "app/common/.gitignore", + "app/proprietary/.gitignore", ".pre-commit-config.yaml" ], "java.signatureHelp.enabled": true, diff --git a/.editorconfig b/.editorconfig index d45455a7a..3f5158dea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -31,18 +31,12 @@ indent_size = 2 # CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards. indent_size = 2 -[*.yaml] +[*.{yml,yaml}] # YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices. indent_size = 2 insert_final_newline = false trim_trailing_whitespace = false -[*.yml] -# YML files follow the same conventions as YAML files, using an indent size of 2 spaces. -indent_size = 2 -insert_final_newline = false -trim_trailing_whitespace = false - [*.json] # JSON files use an indent size of 2 spaces, which is the standard for JSON formatting. indent_size = 2 diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 2f4f242cb..225470ea9 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -26,4 +26,4 @@ project: &project - gradlew - gradlew.bat - launch4jConfig.xml - - settings.gradle \ No newline at end of file + - settings.gradle diff --git a/.github/labels.yml b/.github/labels.yml index 9b35ccb1a..b6cd969f6 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -42,6 +42,7 @@ - name: "Front End" color: "BBD2F1" description: "Issues or pull requests related to front-end development" + from_name: "frontend" - name: "github-actions" description: "Pull requests that update GitHub Actions code" color: "999999" @@ -77,6 +78,7 @@ - name: "Translation" color: "9FABF9" from_name: "translation" + description: "Issues or pull requests related to translation" - name: "upstream" color: "DEDEDE" - name: "v2" @@ -178,3 +180,6 @@ - name: "pr-deployed" color: "00FF00" description: "Pull request has been deployed to a test environment" +- name: "codex" + color: "ededed" + description: "chatgpt AI generated code" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index abc54d43e..5b8f77bbc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "editor.wordSegmenterLocales": "", "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", + "editor.defaultFormatter": "EditorConfig.EditorConfig", "cSpell.enabled": false, "[feature]": { "editor.defaultFormatter": "alexkrechik.cucumberautocomplete" @@ -40,7 +41,7 @@ "java.configuration.updateBuildConfiguration": "interactive", "java.format.enabled": true, "java.format.settings.profile": "GoogleStyle", - "java.format.settings.google.version": "1.27.0", + "java.format.settings.google.version": "1.28.0", "java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting", // (DE) Aktiviert Kommentare im Java-Format. // (EN) Enables comments in Java formatting. diff --git a/build.gradle b/build.gradle index fd9abf7c8..ec786e2ed 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ ext { openSamlVersion = "4.3.2" commonmarkVersion = "0.25.1" googleJavaFormatVersion = "1.28.0" + junitPlatformVersion = "1.12.2" tempJrePath = null } @@ -82,6 +83,7 @@ subprojects { apply plugin: 'com.diffplug.spotless' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' + apply plugin: 'jacoco' java { // 17 is lowest but we support and recommend 21 @@ -125,7 +127,7 @@ subprojects { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' + testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" } tasks.withType(JavaCompile).configureEach { @@ -139,6 +141,16 @@ subprojects { test { useJUnitPlatform() + finalizedBy jacocoTestReport + } + + jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } } tasks.named("processResources") { @@ -556,7 +568,7 @@ dependencies { } testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' + testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" } tasks.named("test") { From 65e894870c07148d9a22d27e9ae962f3679fae83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:14:57 +0200 Subject: [PATCH 54/71] refactor(eml-to-pdf): Improve readability, maintainability, and overall standards compliance (#4065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes refactor(eml-to-pdf): Enhance compliance with PDF/ISO standards and MIME specifications This commit refactors the EML-to-PDF conversion utility to improve standards compliance, implementing requirements from multiple RFCs and ISO specifications: ### Standards Compliance Implemented: • **PDF Standards (ISO 32000-1:2008)**: Added PDF version validation in `attachFilesToPdf()` to ensure 1.7+ compatibility for Unicode file embeddings • **MIME Processing (RFC 2045/2046)**: Implemented case-insensitive MIME type handling in `processPartAdvanced()` with `toLowerCase(Locale.ROOT)` normalization • **Content Encoding (RFC 2047)**: Enhanced `safeMimeDecode()` with UTF-8→ISO-8859-1 charset fallback chains for robust header decoding • **Content-ID Processing (RFC 2392)**: Added proper Content-ID stripping with `replaceAll("[<>]", "")` for embedded image references • **Multipart Safety (RFC 2046)** (best practice, not compliance related): Implemented recursion depth limiting (max 10 levels) • **processMultipartAdvanced()**, setCatalogViewerPreferences used to set PageMode.USE_ATTACHMENTS, but PDF spec 12.2 (Viewer Preferences) requires a /ViewerPreferences dictionary for full control (e.g., /DisplayDocTitle). Docs suggested setting additional prefs like /NonFullScreenPageMode to ensure attachments panel opens reliably across viewers • **addAttachmentAnnotationToPage**, annotations are set to /Invisible=true but must remain interactive. PDF spec 12.5.6.15 (File Attachment Annotations) requires /F flags to control print/view (e.g., NoPrint if not printable). ### Technical Improvements: • **Coordinate System Handling**: Added rotation-aware coordinate transformations in PDF annotation placement following ISO 32000-1 Section 8.3 • **Charset Fallbacks**: Implemented progressive charset detection with UTF-8 primary and ISO-8859-1 fallback in MIME decoding • **Error Resilience**: Enhanced exception handling with specific error types and proper resource cleanup using try-with-resources patterns • **HTML5 Compliance**: Updated email HTML generation with proper DOCTYPE and charset declarations for browser compatibility ### Security & Robustness: • **Input Validation**: Added comprehensive null checks and boundary validation throughout attachment and multipart processing • **XSS Prevention**: All user content now processed through `escapeHtml()` or `CustomHtmlSanitizer` before HTML generation ### Code Quality: • **Method Signatures**: Updated `processMultipartAdvanced()` to include depth parameter for recursion tracking • **Switch Expressions**: Modernized switch statements to use Java 17+ arrow syntax where applicable • **Documentation**: Added inline RFC/ISO references for compliance-critical sections All changes maintain backward compatibility while significantly improving standards adherence. Tested with various EML formats. No major change. No change in tests. No change in aesthetic of the resulting PDF. No change change in "user space" (except when user relied on compliance of aforementioned stuff then a major improvement) --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../software/common/util/EmlParser.java | 652 +++++++ .../common/util/EmlProcessingUtils.java | 601 ++++++ .../software/common/util/EmlToPdf.java | 1655 +---------------- .../common/util/PdfAttachmentHandler.java | 680 +++++++ 4 files changed, 1950 insertions(+), 1638 deletions(-) create mode 100644 app/common/src/main/java/stirling/software/common/util/EmlParser.java create mode 100644 app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java create mode 100644 app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java diff --git a/app/common/src/main/java/stirling/software/common/util/EmlParser.java b/app/common/src/main/java/stirling/software/common/util/EmlParser.java new file mode 100644 index 000000000..0815b1c56 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/EmlParser.java @@ -0,0 +1,652 @@ +package stirling.software.common.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.regex.Pattern; + +import lombok.Data; +import lombok.experimental.UtilityClass; + +import stirling.software.common.model.api.converters.EmlToPdfRequest; + +@UtilityClass +public class EmlParser { + + private static volatile Boolean jakartaMailAvailable = null; + private static volatile Method mimeUtilityDecodeTextMethod = null; + private static volatile boolean mimeUtilityChecked = false; + + private static final Pattern MIME_ENCODED_PATTERN = + Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + + private static final String DISPOSITION_ATTACHMENT = "attachment"; + private static final String TEXT_PLAIN = "text/plain"; + private static final String TEXT_HTML = "text/html"; + private static final String MULTIPART_PREFIX = "multipart/"; + + private static final String HEADER_CONTENT_TYPE = "content-type:"; + private static final String HEADER_CONTENT_DISPOSITION = "content-disposition:"; + private static final String HEADER_CONTENT_TRANSFER_ENCODING = "content-transfer-encoding:"; + private static final String HEADER_CONTENT_ID = "Content-ID"; + private static final String HEADER_SUBJECT = "Subject:"; + private static final String HEADER_FROM = "From:"; + private static final String HEADER_TO = "To:"; + private static final String HEADER_CC = "Cc:"; + private static final String HEADER_BCC = "Bcc:"; + private static final String HEADER_DATE = "Date:"; + + private static synchronized boolean isJakartaMailAvailable() { + if (jakartaMailAvailable == null) { + try { + Class.forName("jakarta.mail.internet.MimeMessage"); + Class.forName("jakarta.mail.Session"); + Class.forName("jakarta.mail.internet.MimeUtility"); + Class.forName("jakarta.mail.internet.MimePart"); + Class.forName("jakarta.mail.internet.MimeMultipart"); + Class.forName("jakarta.mail.Multipart"); + Class.forName("jakarta.mail.Part"); + jakartaMailAvailable = true; + } catch (ClassNotFoundException e) { + jakartaMailAvailable = false; + } + } + return jakartaMailAvailable; + } + + public static EmailContent extractEmailContent( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) + throws IOException { + EmlProcessingUtils.validateEmlInput(emlBytes); + + if (isJakartaMailAvailable()) { + return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer); + } else { + return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + } + } + + private static EmailContent extractEmailContentBasic( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + String emlContent = new String(emlBytes, StandardCharsets.UTF_8); + EmailContent content = new EmailContent(); + + content.setSubject(extractBasicHeader(emlContent, HEADER_SUBJECT)); + content.setFrom(extractBasicHeader(emlContent, HEADER_FROM)); + content.setTo(extractBasicHeader(emlContent, HEADER_TO)); + content.setCc(extractBasicHeader(emlContent, HEADER_CC)); + content.setBcc(extractBasicHeader(emlContent, HEADER_BCC)); + + String dateStr = extractBasicHeader(emlContent, HEADER_DATE); + if (!dateStr.isEmpty()) { + content.setDateString(dateStr); + } + + String htmlBody = extractHtmlBody(emlContent); + if (htmlBody != null) { + content.setHtmlBody(htmlBody); + } else { + String textBody = extractTextBody(emlContent); + content.setTextBody(textBody != null ? textBody : "Email content could not be parsed"); + } + + content.getAttachments().addAll(extractAttachmentsBasic(emlContent)); + + return content; + } + + private static EmailContent extractEmailContentAdvanced( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + try { + Class sessionClass = Class.forName("jakarta.mail.Session"); + Class mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage"); + + Method getDefaultInstance = + sessionClass.getMethod("getDefaultInstance", Properties.class); + Object session = getDefaultInstance.invoke(null, new Properties()); + + Class[] constructorArgs = new Class[] {sessionClass, InputStream.class}; + Constructor mimeMessageConstructor = + mimeMessageClass.getConstructor(constructorArgs); + Object message = + mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes)); + + return extractFromMimeMessage(message, request, customHtmlSanitizer); + + } catch (ReflectiveOperationException e) { + return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + } + } + + private static EmailContent extractFromMimeMessage( + Object message, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + EmailContent content = new EmailContent(); + + try { + Class messageClass = message.getClass(); + + Method getSubject = messageClass.getMethod("getSubject"); + String subject = (String) getSubject.invoke(message); + content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject"); + + Method getFrom = messageClass.getMethod("getFrom"); + Object[] fromAddresses = (Object[]) getFrom.invoke(message); + content.setFrom(buildAddressString(fromAddresses)); + + extractRecipients(message, messageClass, content); + + Method getSentDate = messageClass.getMethod("getSentDate"); + content.setDate((Date) getSentDate.invoke(message)); + + Method getContent = messageClass.getMethod("getContent"); + Object messageContent = getContent.invoke(message); + + processMessageContent(message, messageContent, content, request, customHtmlSanitizer); + + } catch (ReflectiveOperationException | RuntimeException e) { + content.setSubject("Email Conversion"); + content.setFrom("Unknown"); + content.setTo("Unknown"); + content.setCc(""); + content.setBcc(""); + content.setTextBody("Email content could not be parsed with advanced processing"); + } + + return content; + } + + private static void extractRecipients( + Object message, Class messageClass, EmailContent content) { + try { + Method getRecipients = + messageClass.getMethod( + "getRecipients", Class.forName("jakarta.mail.Message$RecipientType")); + Class recipientTypeClass = Class.forName("jakarta.mail.Message$RecipientType"); + + Object toType = recipientTypeClass.getField("TO").get(null); + Object[] toRecipients = (Object[]) getRecipients.invoke(message, toType); + content.setTo(buildAddressString(toRecipients)); + + Object ccType = recipientTypeClass.getField("CC").get(null); + Object[] ccRecipients = (Object[]) getRecipients.invoke(message, ccType); + content.setCc(buildAddressString(ccRecipients)); + + Object bccType = recipientTypeClass.getField("BCC").get(null); + Object[] bccRecipients = (Object[]) getRecipients.invoke(message, bccType); + content.setBcc(buildAddressString(bccRecipients)); + + } catch (ReflectiveOperationException e) { + try { + Method getAllRecipients = messageClass.getMethod("getAllRecipients"); + Object[] recipients = (Object[]) getAllRecipients.invoke(message); + content.setTo(buildAddressString(recipients)); + content.setCc(""); + content.setBcc(""); + } catch (ReflectiveOperationException ex) { + content.setTo(""); + content.setCc(""); + content.setBcc(""); + } + } + } + + private static String buildAddressString(Object[] addresses) { + if (addresses == null || addresses.length == 0) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < addresses.length; i++) { + if (i > 0) builder.append(", "); + builder.append(safeMimeDecode(addresses[i].toString())); + } + return builder.toString(); + } + + private static void processMessageContent( + Object message, + Object messageContent, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + try { + if (messageContent instanceof String stringContent) { + Method getContentType = message.getClass().getMethod("getContentType"); + String contentType = (String) getContentType.invoke(message); + + if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) { + content.setHtmlBody(stringContent); + } else { + content.setTextBody(stringContent); + } + } else { + Class multipartClass = Class.forName("jakarta.mail.Multipart"); + if (multipartClass.isInstance(messageContent)) { + processMultipart(messageContent, content, request, customHtmlSanitizer, 0); + } + } + } catch (ReflectiveOperationException | ClassCastException e) { + content.setTextBody("Email content could not be parsed with advanced processing"); + } + } + + private static void processMultipart( + Object multipart, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer, + int depth) { + + final int MAX_MULTIPART_DEPTH = 10; + if (depth > MAX_MULTIPART_DEPTH) { + content.setHtmlBody("
Maximum multipart depth exceeded
"); + return; + } + + try { + Class multipartClass = multipart.getClass(); + Method getCount = multipartClass.getMethod("getCount"); + int count = (Integer) getCount.invoke(multipart); + + Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class); + + for (int i = 0; i < count; i++) { + Object part = getBodyPart.invoke(multipart, i); + processPart(part, content, request, customHtmlSanitizer, depth + 1); + } + + } catch (ReflectiveOperationException | ClassCastException e) { + content.setHtmlBody("
Error processing multipart content
"); + } + } + + private static void processPart( + Object part, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer, + int depth) { + try { + Class partClass = part.getClass(); + + Method isMimeType = partClass.getMethod("isMimeType", String.class); + Method getContent = partClass.getMethod("getContent"); + Method getDisposition = partClass.getMethod("getDisposition"); + Method getFileName = partClass.getMethod("getFileName"); + Method getContentType = partClass.getMethod("getContentType"); + Method getHeader = partClass.getMethod("getHeader", String.class); + + Object disposition = getDisposition.invoke(part); + String filename = (String) getFileName.invoke(part); + String contentType = (String) getContentType.invoke(part); + + String normalizedDisposition = + disposition != null ? ((String) disposition).toLowerCase() : null; + + if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) { + Object partContent = getContent.invoke(part); + if (partContent instanceof String stringContent) { + content.setTextBody(stringContent); + } + } else if ((Boolean) isMimeType.invoke(part, TEXT_HTML) + && normalizedDisposition == null) { + Object partContent = getContent.invoke(part); + if (partContent instanceof String stringContent) { + String htmlBody = + customHtmlSanitizer != null + ? customHtmlSanitizer.sanitize(stringContent) + : stringContent; + content.setHtmlBody(htmlBody); + } + } else if ((normalizedDisposition != null + && normalizedDisposition.contains(DISPOSITION_ATTACHMENT)) + || (filename != null && !filename.trim().isEmpty())) { + + processAttachment( + part, content, request, getHeader, getContent, filename, contentType); + } else if ((Boolean) isMimeType.invoke(part, "multipart/*")) { + Object multipartContent = getContent.invoke(part); + if (multipartContent != null) { + Class multipartClass = Class.forName("jakarta.mail.Multipart"); + if (multipartClass.isInstance(multipartContent)) { + processMultipart( + multipartContent, content, request, customHtmlSanitizer, depth + 1); + } + } + } + + } catch (ReflectiveOperationException | RuntimeException e) { + // Continue processing other parts if one fails + } + } + + private static void processAttachment( + Object part, + EmailContent content, + EmlToPdfRequest request, + Method getHeader, + Method getContent, + String filename, + String contentType) { + + content.setAttachmentCount(content.getAttachmentCount() + 1); + + if (filename != null && !filename.trim().isEmpty()) { + EmailAttachment attachment = new EmailAttachment(); + attachment.setFilename(safeMimeDecode(filename)); + attachment.setContentType(contentType); + + try { + String[] contentIdHeaders = (String[]) getHeader.invoke(part, HEADER_CONTENT_ID); + if (contentIdHeaders != null) { + for (String contentIdHeader : contentIdHeaders) { + if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) { + attachment.setEmbedded(true); + String contentId = contentIdHeader.trim().replaceAll("[<>]", ""); + attachment.setContentId(contentId); + break; + } + } + } + } catch (ReflectiveOperationException e) { + } + + if ((request != null && request.isIncludeAttachments()) || attachment.isEmbedded()) { + extractAttachmentData(part, attachment, getContent, request); + } + + content.getAttachments().add(attachment); + } + } + + private static void extractAttachmentData( + Object part, EmailAttachment attachment, Method getContent, EmlToPdfRequest request) { + try { + Object attachmentContent = getContent.invoke(part); + byte[] attachmentData = null; + + if (attachmentContent instanceof InputStream inputStream) { + try (InputStream stream = inputStream) { + attachmentData = stream.readAllBytes(); + } catch (IOException e) { + if (attachment.isEmbedded()) { + attachmentData = new byte[0]; + } else { + throw new RuntimeException(e); + } + } + } else if (attachmentContent instanceof byte[] byteArray) { + attachmentData = byteArray; + } else if (attachmentContent instanceof String stringContent) { + attachmentData = stringContent.getBytes(StandardCharsets.UTF_8); + } + + if (attachmentData != null) { + long maxSizeMB = request != null ? request.getMaxAttachmentSizeMB() : 10L; + long maxSizeBytes = maxSizeMB * 1024 * 1024; + + if (attachmentData.length <= maxSizeBytes || attachment.isEmbedded()) { + attachment.setData(attachmentData); + attachment.setSizeBytes(attachmentData.length); + } else { + attachment.setSizeBytes(attachmentData.length); + } + } + } catch (ReflectiveOperationException | RuntimeException e) { + // Continue without attachment data + } + } + + private static String extractBasicHeader(String emlContent, String headerName) { + try { + String[] lines = emlContent.split("\r?\n"); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (line.toLowerCase().startsWith(headerName.toLowerCase())) { + StringBuilder value = + new StringBuilder(line.substring(headerName.length()).trim()); + for (int j = i + 1; j < lines.length; j++) { + if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) { + value.append(" ").append(lines[j].trim()); + } else { + break; + } + } + return safeMimeDecode(value.toString()); + } + if (line.trim().isEmpty()) break; + } + } catch (RuntimeException e) { + // Ignore errors in header extraction + } + return ""; + } + + private static String extractHtmlBody(String emlContent) { + try { + String lowerContent = emlContent.toLowerCase(); + int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML); + if (htmlStart == -1) return null; + + int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart); + if (bodyStart == -1) return null; + + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + + return emlContent.substring(bodyStart, bodyEnd).trim(); + } catch (Exception e) { + return null; + } + } + + private static String extractTextBody(String emlContent) { + try { + String lowerContent = emlContent.toLowerCase(); + int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN); + if (textStart == -1) { + int bodyStart = emlContent.indexOf("\r\n\r\n"); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n"); + if (bodyStart != -1) { + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + return emlContent.substring(bodyStart, bodyEnd).trim(); + } + return null; + } + + int bodyStart = emlContent.indexOf("\r\n\r\n", textStart); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", textStart); + if (bodyStart == -1) return null; + + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + + return emlContent.substring(bodyStart, bodyEnd).trim(); + } catch (RuntimeException e) { + return null; + } + } + + private static int findPartEnd(String content, int start) { + String[] lines = content.substring(start).split("\r?\n"); + StringBuilder result = new StringBuilder(); + + for (String line : lines) { + if (line.startsWith("--") && line.length() > 10) break; + result.append(line).append("\n"); + } + + return start + result.length(); + } + + private static List extractAttachmentsBasic(String emlContent) { + List attachments = new ArrayList<>(); + try { + String[] lines = emlContent.split("\r?\n"); + boolean inHeaders = true; + String currentContentType = ""; + String currentDisposition = ""; + String currentFilename = ""; + String currentEncoding = ""; + + for (String line : lines) { + String lowerLine = line.toLowerCase().trim(); + + if (line.trim().isEmpty()) { + inHeaders = false; + if (isAttachment(currentDisposition, currentFilename, currentContentType)) { + EmailAttachment attachment = new EmailAttachment(); + attachment.setFilename(currentFilename); + attachment.setContentType(currentContentType); + attachment.setTransferEncoding(currentEncoding); + attachments.add(attachment); + } + currentContentType = ""; + currentDisposition = ""; + currentFilename = ""; + currentEncoding = ""; + inHeaders = true; + continue; + } + + if (!inHeaders) continue; + + if (lowerLine.startsWith(HEADER_CONTENT_TYPE)) { + currentContentType = line.substring(HEADER_CONTENT_TYPE.length()).trim(); + } else if (lowerLine.startsWith(HEADER_CONTENT_DISPOSITION)) { + currentDisposition = line.substring(HEADER_CONTENT_DISPOSITION.length()).trim(); + currentFilename = extractFilenameFromDisposition(currentDisposition); + } else if (lowerLine.startsWith(HEADER_CONTENT_TRANSFER_ENCODING)) { + currentEncoding = + line.substring(HEADER_CONTENT_TRANSFER_ENCODING.length()).trim(); + } + } + } catch (RuntimeException e) { + // Continue with empty list + } + return attachments; + } + + private static boolean isAttachment(String disposition, String filename, String contentType) { + return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty()) + || (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/")) + || (contentType.toLowerCase().contains("application/") && !filename.isEmpty()); + } + + private static String extractFilenameFromDisposition(String disposition) { + if (disposition == null || !disposition.contains("filename=")) { + return ""; + } + + // Handle filename*= (RFC 2231 encoded filename) + if (disposition.toLowerCase().contains("filename*=")) { + int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10; + int filenameStarEnd = disposition.indexOf(";", filenameStarStart); + if (filenameStarEnd == -1) filenameStarEnd = disposition.length(); + String extendedFilename = + disposition.substring(filenameStarStart, filenameStarEnd).trim(); + extendedFilename = extendedFilename.replaceAll("^\"|\"$", ""); + + if (extendedFilename.contains("'")) { + String[] parts = extendedFilename.split("'", 3); + if (parts.length == 3) { + return EmlProcessingUtils.decodeUrlEncoded(parts[2]); + } + } + } + + // Handle regular filename= + int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9; + int filenameEnd = disposition.indexOf(";", filenameStart); + if (filenameEnd == -1) filenameEnd = disposition.length(); + String filename = disposition.substring(filenameStart, filenameEnd).trim(); + filename = filename.replaceAll("^\"|\"$", ""); + return safeMimeDecode(filename); + } + + public static String safeMimeDecode(String headerValue) { + if (headerValue == null || headerValue.trim().isEmpty()) { + return ""; + } + + if (!mimeUtilityChecked) { + synchronized (EmlParser.class) { + if (!mimeUtilityChecked) { + initializeMimeUtilityDecoding(); + } + } + } + + if (mimeUtilityDecodeTextMethod != null) { + try { + return (String) mimeUtilityDecodeTextMethod.invoke(null, headerValue.trim()); + } catch (ReflectiveOperationException | RuntimeException e) { + // Fall through to custom implementation + } + } + + return EmlProcessingUtils.decodeMimeHeader(headerValue.trim()); + } + + private static void initializeMimeUtilityDecoding() { + try { + Class mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility"); + mimeUtilityDecodeTextMethod = mimeUtilityClass.getMethod("decodeText", String.class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + mimeUtilityDecodeTextMethod = null; + } + mimeUtilityChecked = true; + } + + @Data + public static class EmailContent { + private String subject; + private String from; + private String to; + private String cc; + private String bcc; + private Date date; + private String dateString; // For basic parsing fallback + private String htmlBody; + private String textBody; + private int attachmentCount; + private List attachments = new ArrayList<>(); + + public void setHtmlBody(String htmlBody) { + this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null; + } + + public void setTextBody(String textBody) { + this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null; + } + } + + @Data + public static class EmailAttachment { + private String filename; + private String contentType; + private byte[] data; + private boolean embedded; + private String embeddedFilename; + private long sizeBytes; + private String contentId; + private String disposition; + private String transferEncoding; + + public void setData(byte[] data) { + this.data = data; + if (data != null) { + this.sizeBytes = data.length; + } + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java new file mode 100644 index 000000000..9acc30c16 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java @@ -0,0 +1,601 @@ +package stirling.software.common.util; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.experimental.UtilityClass; + +import stirling.software.common.model.api.converters.EmlToPdfRequest; +import stirling.software.common.model.api.converters.HTMLToPdfRequest; + +@UtilityClass +public class EmlProcessingUtils { + + // Style constants + private static final int DEFAULT_FONT_SIZE = 12; + private static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif"; + private static final float DEFAULT_LINE_HEIGHT = 1.4f; + private static final String DEFAULT_ZOOM = "1.0"; + private static final String DEFAULT_TEXT_COLOR = "#202124"; + private static final String DEFAULT_BACKGROUND_COLOR = "#ffffff"; + private static final String DEFAULT_BORDER_COLOR = "#e8eaed"; + private static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9"; + private static final String ATTACHMENT_BORDER_COLOR = "#eeeeee"; + + private static final int EML_CHECK_LENGTH = 8192; + private static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2; + + // MIME type detection + private static final Map EXTENSION_TO_MIME_TYPE = + Map.of( + ".png", "image/png", + ".jpg", "image/jpeg", + ".jpeg", "image/jpeg", + ".gif", "image/gif", + ".bmp", "image/bmp", + ".webp", "image/webp", + ".svg", "image/svg+xml", + ".ico", "image/x-icon", + ".tiff", "image/tiff", + ".tif", "image/tiff"); + + public static void validateEmlInput(byte[] emlBytes) { + if (emlBytes == null || emlBytes.length == 0) { + throw new IllegalArgumentException("EML file is empty or null"); + } + + if (isInvalidEmlFormat(emlBytes)) { + throw new IllegalArgumentException("Invalid EML file format"); + } + } + + private static boolean isInvalidEmlFormat(byte[] emlBytes) { + try { + int checkLength = Math.min(emlBytes.length, EML_CHECK_LENGTH); + String content; + + try { + content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8); + if (content.contains("\uFFFD")) { + content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1); + } + } catch (Exception e) { + content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1); + } + + String lowerContent = content.toLowerCase(Locale.ROOT); + + boolean hasFrom = + lowerContent.contains("from:") || lowerContent.contains("return-path:"); + boolean hasSubject = lowerContent.contains("subject:"); + boolean hasMessageId = lowerContent.contains("message-id:"); + boolean hasDate = lowerContent.contains("date:"); + boolean hasTo = + lowerContent.contains("to:") + || lowerContent.contains("cc:") + || lowerContent.contains("bcc:"); + boolean hasMimeStructure = + lowerContent.contains("multipart/") + || lowerContent.contains("text/plain") + || lowerContent.contains("text/html") + || lowerContent.contains("boundary="); + + int headerCount = 0; + if (hasFrom) headerCount++; + if (hasSubject) headerCount++; + if (hasMessageId) headerCount++; + if (hasDate) headerCount++; + if (hasTo) headerCount++; + + return headerCount < MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure; + + } catch (RuntimeException e) { + return false; + } + } + + public static String generateEnhancedEmailHtml( + EmlParser.EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + StringBuilder html = new StringBuilder(); + + html.append( + String.format( + """ + + + %s + + + """); + + html.append( + String.format( + """ + \n"); + return html.toString(); + } + + public static String processEmailHtmlBody( + String htmlBody, + EmlParser.EmailContent emailContent, + CustomHtmlSanitizer customHtmlSanitizer) { + if (htmlBody == null) return ""; + + String processed = + customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody; + + processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", ""); + processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", ""); + + if (emailContent != null && !emailContent.getAttachments().isEmpty()) { + processed = PdfAttachmentHandler.processInlineImages(processed, emailContent); + } + + return processed; + } + + public static String convertTextToHtml( + String textBody, CustomHtmlSanitizer customHtmlSanitizer) { + if (textBody == null) return ""; + + String html = + customHtmlSanitizer != null + ? customHtmlSanitizer.sanitize(textBody) + : escapeHtml(textBody); + + html = html.replace("\r\n", "\n").replace("\r", "\n"); + html = html.replace("\n", "
\n"); + + html = + html.replaceAll( + "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", + "$1"); + + html = + html.replaceAll( + "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})", + "$1"); + + return html; + } + + private static void appendEnhancedStyles(StringBuilder html) { + String css = + String.format( + """ + body { + font-family: %s; + font-size: %dpx; + line-height: %s; + color: %s; + margin: 0; + padding: 16px; + background-color: %s; + } + + .email-container { + width: 100%%; + max-width: 100%%; + margin: 0 auto; + } + + .email-header { + padding-bottom: 10px; + border-bottom: 1px solid %s; + margin-bottom: 10px; + } + + .email-header h1 { + margin: 0 0 10px 0; + font-size: %dpx; + font-weight: bold; + } + + .email-meta div { + margin-bottom: 2px; + font-size: %dpx; + } + + .email-body { + word-wrap: break-word; + } + + .attachment-section { + margin-top: 15px; + padding: 10px; + background-color: %s; + border: 1px solid %s; + border-radius: 3px; + } + + .attachment-section h3 { + margin: 0 0 8px 0; + font-size: %dpx; + } + + .attachment-item { + padding: 5px 0; + } + + .attachment-icon { + margin-right: 5px; + } + + .attachment-details, .attachment-type { + font-size: %dpx; + color: #555555; + } + + .attachment-inclusion-note, .attachment-info-note { + margin-top: 8px; + padding: 6px; + font-size: %dpx; + border-radius: 3px; + } + + .attachment-inclusion-note { + background-color: #e6ffed; + border: 1px solid #d4f7dc; + color: #006420; + } + + .attachment-info-note { + background-color: #fff9e6; + border: 1px solid #fff0c2; + color: #664d00; + } + + .attachment-link-container { + display: flex; + align-items: center; + padding: 8px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + margin: 4px 0; + } + + .attachment-link-container:hover { + background-color: #e9ecef; + } + + .attachment-note { + font-size: %dpx; + color: #6c757d; + font-style: italic; + margin-left: 8px; + } + + .no-content { + padding: 20px; + text-align: center; + color: #666; + font-style: italic; + } + + .text-body { + white-space: pre-wrap; + } + + img { + max-width: 100%%; + height: auto; + display: block; + } + """, + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + DEFAULT_LINE_HEIGHT, + DEFAULT_TEXT_COLOR, + DEFAULT_BACKGROUND_COLOR, + DEFAULT_BORDER_COLOR, + DEFAULT_FONT_SIZE + 4, + DEFAULT_FONT_SIZE - 1, + ATTACHMENT_BACKGROUND_COLOR, + ATTACHMENT_BORDER_COLOR, + DEFAULT_FONT_SIZE + 1, + DEFAULT_FONT_SIZE - 2, + DEFAULT_FONT_SIZE - 2, + DEFAULT_FONT_SIZE - 3); + + html.append(css); + } + + private static void appendAttachmentsSection( + StringBuilder html, + EmlParser.EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + html.append("
\n"); + int displayedAttachmentCount = + content.getAttachmentCount() > 0 + ? content.getAttachmentCount() + : content.getAttachments().size(); + html.append("

Attachments (").append(displayedAttachmentCount).append(")

\n"); + + if (!content.getAttachments().isEmpty()) { + for (int i = 0; i < content.getAttachments().size(); i++) { + EmlParser.EmailAttachment attachment = content.getAttachments().get(i); + + String embeddedFilename = + attachment.getFilename() != null + ? attachment.getFilename() + : ("attachment_" + i); + attachment.setEmbeddedFilename(embeddedFilename); + + String sizeStr = GeneralUtils.formatBytes(attachment.getSizeBytes()); + String contentType = + attachment.getContentType() != null + && !attachment.getContentType().isEmpty() + ? ", " + escapeHtml(attachment.getContentType()) + : ""; + + String attachmentId = "attachment_" + i; + html.append( + String.format( + """ +
+ @ + %s + (%s%s) +
+ """, + attachmentId, + escapeHtml(embeddedFilename), + escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())), + sizeStr, + contentType)); + } + } + + if (request != null && request.isIncludeAttachments()) { + html.append( + """ +
+

Attachments are embedded in the file.

+
+ """); + } else { + html.append( + """ +
+

Attachment information displayed - files not included in PDF.

+
+ """); + } + html.append("
\n"); + } + + public static HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) { + HTMLToPdfRequest htmlRequest = new HTMLToPdfRequest(); + + if (request != null) { + htmlRequest.setFileInput(request.getFileInput()); + } + + htmlRequest.setZoom(Float.parseFloat(DEFAULT_ZOOM)); + return htmlRequest; + } + + public static String detectMimeType(String filename, String existingMimeType) { + if (existingMimeType != null && !existingMimeType.isEmpty()) { + return existingMimeType; + } + + if (filename != null) { + String lowerFilename = filename.toLowerCase(); + for (Map.Entry entry : EXTENSION_TO_MIME_TYPE.entrySet()) { + if (lowerFilename.endsWith(entry.getKey())) { + return entry.getValue(); + } + } + } + + return "image/png"; + } + + public static String decodeUrlEncoded(String encoded) { + try { + return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } catch (Exception e) { + return encoded; // Return original if decoding fails + } + } + + public static String decodeMimeHeader(String encodedText) { + if (encodedText == null || encodedText.trim().isEmpty()) { + return encodedText; + } + + try { + StringBuilder result = new StringBuilder(); + Pattern concatenatedPattern = + Pattern.compile( + "(=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)(\\s*=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)+"); + Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText); + String processedText = + concatenatedMatcher.replaceAll( + match -> match.group().replaceAll("\\s+(?==\\?)", "")); + + Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + Matcher matcher = mimePattern.matcher(processedText); + int lastEnd = 0; + + while (matcher.find()) { + result.append(processedText, lastEnd, matcher.start()); + + String charset = matcher.group(1); + String encoding = matcher.group(2).toUpperCase(); + String encodedValue = matcher.group(3); + + try { + String decodedValue = + switch (encoding) { + case "B" -> { + String cleanBase64 = encodedValue.replaceAll("\\s", ""); + byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64); + Charset targetCharset; + try { + targetCharset = Charset.forName(charset); + } catch (Exception e) { + targetCharset = StandardCharsets.UTF_8; + } + yield new String(decodedBytes, targetCharset); + } + case "Q" -> decodeQuotedPrintable(encodedValue, charset); + default -> matcher.group(0); // Return original if unknown encoding + }; + result.append(decodedValue); + } catch (RuntimeException e) { + result.append(matcher.group(0)); // Keep original on decode error + } + + lastEnd = matcher.end(); + } + + result.append(processedText.substring(lastEnd)); + return result.toString(); + } catch (Exception e) { + return encodedText; // Return original on any parsing error + } + } + + private static String decodeQuotedPrintable(String encodedText, String charset) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < encodedText.length(); i++) { + char c = encodedText.charAt(i); + switch (c) { + case '=' -> { + if (i + 2 < encodedText.length()) { + String hex = encodedText.substring(i + 1, i + 3); + try { + int value = Integer.parseInt(hex, 16); + result.append((char) value); + i += 2; + } catch (NumberFormatException e) { + result.append(c); + } + } else if (i + 1 == encodedText.length() + || (i + 2 == encodedText.length() + && encodedText.charAt(i + 1) == '\n')) { + if (i + 1 < encodedText.length() && encodedText.charAt(i + 1) == '\n') { + i++; // Skip the newline too + } + } else { + result.append(c); + } + } + case '_' -> result.append(' '); // Space encoding in Q encoding + default -> result.append(c); + } + } + + byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1); + try { + Charset targetCharset = Charset.forName(charset); + return new String(bytes, targetCharset); + } catch (Exception e) { + try { + return new String(bytes, StandardCharsets.UTF_8); + } catch (Exception fallbackException) { + return new String(bytes, StandardCharsets.ISO_8859_1); + } + } + } + + public static String escapeHtml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + public static String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) { + if (customHtmlSanitizer != null) { + return customHtmlSanitizer.sanitize(text); + } else { + return escapeHtml(text); + } + } + + public static String simplifyHtmlContent(String htmlContent) { + String simplified = htmlContent.replaceAll("(?i)]*>.*?", ""); + simplified = simplified.replaceAll("(?i)]*>.*?", ""); + return simplified; + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java index 6b28dc683..85005af40 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java @@ -1,131 +1,23 @@ package stirling.software.common.util; -import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; -import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PageMode; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; -import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import lombok.Data; -import lombok.Getter; import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.api.converters.EmlToPdfRequest; -import stirling.software.common.model.api.converters.HTMLToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; -@Slf4j @UtilityClass public class EmlToPdf { - private static final class StyleConstants { - // Font and layout constants - static final int DEFAULT_FONT_SIZE = 12; - static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif"; - static final float DEFAULT_LINE_HEIGHT = 1.4f; - static final String DEFAULT_ZOOM = "1.0"; - - // Color constants - aligned with application theme - static final String DEFAULT_TEXT_COLOR = "#202124"; - static final String DEFAULT_BACKGROUND_COLOR = "#ffffff"; - static final String DEFAULT_BORDER_COLOR = "#e8eaed"; - static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9"; - static final String ATTACHMENT_BORDER_COLOR = "#eeeeee"; - - // Size constants for PDF annotations - static final float ATTACHMENT_ICON_WIDTH = 12f; - static final float ATTACHMENT_ICON_HEIGHT = 14f; - static final float ANNOTATION_X_OFFSET = 2f; - static final float ANNOTATION_Y_OFFSET = 10f; - - // Content validation constants - static final int EML_CHECK_LENGTH = 8192; - static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2; - - private StyleConstants() {} - } - - private static final class MimeConstants { - static final Pattern MIME_ENCODED_PATTERN = - Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); - static final String ATTACHMENT_MARKER = "@"; - - private MimeConstants() {} - } - - private static final class FileSizeConstants { - static final long BYTES_IN_KB = 1024L; - static final long BYTES_IN_MB = BYTES_IN_KB * 1024L; - static final long BYTES_IN_GB = BYTES_IN_MB * 1024L; - - private FileSizeConstants() {} - } - - // Cached Jakarta Mail availability check - private static Boolean jakartaMailAvailable = null; - - private static boolean isJakartaMailAvailable() { - if (jakartaMailAvailable == null) { - try { - // Check for core Jakarta Mail classes - Class.forName("jakarta.mail.internet.MimeMessage"); - Class.forName("jakarta.mail.Session"); - Class.forName("jakarta.mail.internet.MimeUtility"); - Class.forName("jakarta.mail.internet.MimePart"); - Class.forName("jakarta.mail.internet.MimeMultipart"); - Class.forName("jakarta.mail.Multipart"); - Class.forName("jakarta.mail.Part"); - - jakartaMailAvailable = true; - log.debug("Jakarta Mail libraries are available"); - } catch (ClassNotFoundException e) { - jakartaMailAvailable = false; - log.debug("Jakarta Mail libraries are not available, using basic parsing"); - } - } - return jakartaMailAvailable; - } - public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request) throws IOException { - validateEmlInput(emlBytes); + EmlProcessingUtils.validateEmlInput(emlBytes); - if (isJakartaMailAvailable()) { - return convertEmlToHtmlAdvanced(emlBytes, request); - } else { - return convertEmlToHtmlBasic(emlBytes, request); - } + EmlParser.EmailContent emailContent = + EmlParser.extractEmailContent(emlBytes, request, null); + return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null); } public static byte[] convertEmlToPdf( @@ -133,26 +25,21 @@ public class EmlToPdf { EmlToPdfRequest request, byte[] emlBytes, String fileName, - stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory, + CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager, CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { - validateEmlInput(emlBytes); + EmlProcessingUtils.validateEmlInput(emlBytes); try { - // Generate HTML representation - EmailContent emailContent = null; - String htmlContent; + EmlParser.EmailContent emailContent = + EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer); - if (isJakartaMailAvailable()) { - emailContent = extractEmailContentAdvanced(emlBytes, request); - htmlContent = generateEnhancedEmailHtml(emailContent, request); - } else { - htmlContent = convertEmlToHtmlBasic(emlBytes, request); - } + String htmlContent = + EmlProcessingUtils.generateEnhancedEmailHtml( + emailContent, request, customHtmlSanitizer); - // Convert HTML to PDF byte[] pdfBytes = convertHtmlToPdf( weasyprintPath, @@ -161,35 +48,23 @@ public class EmlToPdf { tempFileManager, customHtmlSanitizer); - // Attach files if available and requested if (shouldAttachFiles(emailContent, request)) { pdfBytes = - attachFilesToPdf( + PdfAttachmentHandler.attachFilesToPdf( pdfBytes, emailContent.getAttachments(), pdfDocumentFactory); } return pdfBytes; } catch (IOException | InterruptedException e) { - log.error("Failed to convert EML to PDF for file: {}", fileName, e); throw e; } catch (Exception e) { - log.error("Unexpected error during EML to PDF conversion for file: {}", fileName, e); - throw new IOException("Conversion failed: " + e.getMessage(), e); + throw new IOException("Error converting EML to PDF", e); } } - private static void validateEmlInput(byte[] emlBytes) { - if (emlBytes == null || emlBytes.length == 0) { - throw new IllegalArgumentException("EML file is empty or null"); - } - - if (isInvalidEmlFormat(emlBytes)) { - throw new IllegalArgumentException("Invalid EML file format"); - } - } - - private static boolean shouldAttachFiles(EmailContent emailContent, EmlToPdfRequest request) { + private static boolean shouldAttachFiles( + EmlParser.EmailContent emailContent, EmlToPdfRequest request) { return emailContent != null && request != null && request.isIncludeAttachments() @@ -204,7 +79,7 @@ public class EmlToPdf { CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { - HTMLToPdfRequest htmlRequest = createHtmlRequest(request); + var htmlRequest = EmlProcessingUtils.createHtmlRequest(request); try { return FileToPdf.convertHtmlToPdf( @@ -215,8 +90,7 @@ public class EmlToPdf { tempFileManager, customHtmlSanitizer); } catch (IOException | InterruptedException e) { - log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML"); - String simplifiedHtml = simplifyHtmlContent(htmlContent); + String simplifiedHtml = EmlProcessingUtils.simplifyHtmlContent(htmlContent); return FileToPdf.convertHtmlToPdf( weasyprintPath, htmlRequest, @@ -226,1499 +100,4 @@ public class EmlToPdf { customHtmlSanitizer); } } - - private static String simplifyHtmlContent(String htmlContent) { - String simplified = htmlContent.replaceAll("(?i)]*>.*?", ""); - simplified = simplified.replaceAll("(?i)]*>.*?", ""); - return simplified; - } - - private static String generateUniqueAttachmentId(String filename) { - return "attachment_" + filename.hashCode() + "_" + System.nanoTime(); - } - - private static String convertEmlToHtmlBasic(byte[] emlBytes, EmlToPdfRequest request) { - if (emlBytes == null || emlBytes.length == 0) { - throw new IllegalArgumentException("EML file is empty or null"); - } - - String emlContent = new String(emlBytes, StandardCharsets.UTF_8); - - // Basic email parsing - String subject = extractBasicHeader(emlContent, "Subject:"); - String from = extractBasicHeader(emlContent, "From:"); - String to = extractBasicHeader(emlContent, "To:"); - String cc = extractBasicHeader(emlContent, "Cc:"); - String bcc = extractBasicHeader(emlContent, "Bcc:"); - String date = extractBasicHeader(emlContent, "Date:"); - - // Try to extract HTML content - String htmlBody = extractHtmlBody(emlContent); - if (htmlBody == null) { - String textBody = extractTextBody(emlContent); - htmlBody = - convertTextToHtml( - textBody != null ? textBody : "Email content could not be parsed"); - } - - // Generate HTML with custom styling based on request - StringBuilder html = new StringBuilder(); - html.append("\n"); - html.append("\n"); - html.append("").append(escapeHtml(subject)).append("\n"); - html.append("\n"); - html.append("\n"); - - html.append("
\n"); - html.append("
\n"); - html.append("

").append(escapeHtml(subject)).append("

\n"); - html.append("
\n"); - html.append("
From: ").append(escapeHtml(from)).append("
\n"); - html.append("
To: ").append(escapeHtml(to)).append("
\n"); - - // Include CC and BCC if present and requested - if (request != null && request.isIncludeAllRecipients()) { - if (!cc.trim().isEmpty()) { - html.append("
CC: ").append(escapeHtml(cc)).append("
\n"); - } - if (!bcc.trim().isEmpty()) { - html.append("
BCC: ") - .append(escapeHtml(bcc)) - .append("
\n"); - } - } - - if (!date.trim().isEmpty()) { - html.append("
Date: ").append(escapeHtml(date)).append("
\n"); - } - html.append("
\n"); - - html.append("
\n"); - html.append(processEmailHtmlBody(htmlBody)); - html.append("
\n"); - - // Add attachment information - always check for and display attachments - String attachmentInfo = extractAttachmentInfo(emlContent); - if (!attachmentInfo.isEmpty()) { - html.append("
\n"); - html.append("

Attachments

\n"); - html.append(attachmentInfo); - - // Add a status message about attachment inclusion - if (request != null && request.isIncludeAttachments()) { - html.append("
\n"); - html.append( - "

Note: Attachments are saved as external files and linked in this PDF. Click the links to open files externally.

\n"); - html.append("
\n"); - } else { - html.append("
\n"); - html.append( - "

Attachment information displayed - files not included in PDF. Enable 'Include attachments' to embed files.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - } - - // Show advanced features status if requested - assert request != null; - if (request.getFileInput().isEmpty()) { - html.append("
\n"); - html.append( - "

Note: Some advanced features require Jakarta Mail dependencies.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - html.append(""); - - return html.toString(); - } - - private static EmailContent extractEmailContentAdvanced( - byte[] emlBytes, EmlToPdfRequest request) { - try { - // Use Jakarta Mail for processing - Class sessionClass = Class.forName("jakarta.mail.Session"); - Class mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage"); - - Method getDefaultInstance = - sessionClass.getMethod("getDefaultInstance", Properties.class); - Object session = getDefaultInstance.invoke(null, new Properties()); - - // Cast the session object to the proper type for the constructor - Class[] constructorArgs = new Class[] {sessionClass, InputStream.class}; - Constructor mimeMessageConstructor = - mimeMessageClass.getConstructor(constructorArgs); - Object message = - mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes)); - - return extractEmailContentAdvanced(message, request); - - } catch (ReflectiveOperationException e) { - // Create basic EmailContent from basic processing - EmailContent content = new EmailContent(); - content.setHtmlBody(convertEmlToHtmlBasic(emlBytes, request)); - return content; - } - } - - private static String convertEmlToHtmlAdvanced(byte[] emlBytes, EmlToPdfRequest request) { - EmailContent content = extractEmailContentAdvanced(emlBytes, request); - return generateEnhancedEmailHtml(content, request); - } - - private static String extractAttachmentInfo(String emlContent) { - StringBuilder attachmentInfo = new StringBuilder(); - try { - String[] lines = emlContent.split("\r?\n"); - boolean inHeaders = true; - String currentContentType = ""; - String currentDisposition = ""; - String currentFilename = ""; - String currentEncoding = ""; - boolean inMultipart = false; - String boundary = ""; - - // First pass: find boundary for multipart messages - for (String line : lines) { - String lowerLine = line.toLowerCase().trim(); - if (lowerLine.startsWith("content-type:") && lowerLine.contains("multipart")) { - if (lowerLine.contains("boundary=")) { - int boundaryStart = lowerLine.indexOf("boundary=") + 9; - String boundaryPart = line.substring(boundaryStart).trim(); - if (boundaryPart.startsWith("\"")) { - boundary = boundaryPart.substring(1, boundaryPart.indexOf("\"", 1)); - } else { - int spaceIndex = boundaryPart.indexOf(" "); - boundary = - spaceIndex > 0 - ? boundaryPart.substring(0, spaceIndex) - : boundaryPart; - } - inMultipart = true; - break; - } - } - if (line.trim().isEmpty()) break; - } - - // Second pass: extract attachment information - for (String line : lines) { - String lowerLine = line.toLowerCase().trim(); - - // Check for boundary markers in multipart messages - if (inMultipart && line.trim().startsWith("--" + boundary)) { - // Reset for new part - currentContentType = ""; - currentDisposition = ""; - currentFilename = ""; - currentEncoding = ""; - inHeaders = true; - continue; - } - - if (inHeaders && line.trim().isEmpty()) { - inHeaders = false; - - // Process accumulated attachment info - if (isAttachment(currentDisposition, currentFilename, currentContentType)) { - addAttachmentToInfo( - attachmentInfo, - currentFilename, - currentContentType, - currentEncoding); - - // Reset for next attachment - currentContentType = ""; - currentDisposition = ""; - currentFilename = ""; - currentEncoding = ""; - } - continue; - } - - if (!inHeaders) continue; // Skip body content - - // Parse headers - if (lowerLine.startsWith("content-type:")) { - currentContentType = line.substring(13).trim(); - } else if (lowerLine.startsWith("content-disposition:")) { - currentDisposition = line.substring(20).trim(); - // Extract filename if present - currentFilename = extractFilenameFromDisposition(currentDisposition); - } else if (lowerLine.startsWith("content-transfer-encoding:")) { - currentEncoding = line.substring(26).trim(); - } else if (line.startsWith(" ") || line.startsWith("\t")) { - // Continuation of previous header - if (currentDisposition.contains("filename=")) { - currentDisposition += " " + line.trim(); - currentFilename = extractFilenameFromDisposition(currentDisposition); - } else if (!currentContentType.isEmpty()) { - currentContentType += " " + line.trim(); - } - } - } - - if (isAttachment(currentDisposition, currentFilename, currentContentType)) { - addAttachmentToInfo( - attachmentInfo, currentFilename, currentContentType, currentEncoding); - } - - } catch (RuntimeException e) { - log.warn("Error extracting attachment info: {}", e.getMessage()); - } - return attachmentInfo.toString(); - } - - private static boolean isAttachment(String disposition, String filename, String contentType) { - return (disposition.toLowerCase().contains("attachment") && !filename.isEmpty()) - || (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/")) - || (contentType.toLowerCase().contains("application/") && !filename.isEmpty()); - } - - private static String extractFilenameFromDisposition(String disposition) { - if (disposition.contains("filename=")) { - int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9; - int filenameEnd = disposition.indexOf(";", filenameStart); - if (filenameEnd == -1) filenameEnd = disposition.length(); - String filename = disposition.substring(filenameStart, filenameEnd).trim(); - filename = filename.replaceAll("^\"|\"$", ""); - // Apply MIME decoding to handle encoded filenames - return safeMimeDecode(filename); - } - return ""; - } - - private static void addAttachmentToInfo( - StringBuilder attachmentInfo, String filename, String contentType, String encoding) { - // Create attachment info with paperclip emoji before filename - attachmentInfo - .append("
") - .append("") - .append(MimeConstants.ATTACHMENT_MARKER) - .append(" ") - .append("") - .append(escapeHtml(filename)) - .append(""); - - // Add content type and encoding info - if (!contentType.isEmpty() || !encoding.isEmpty()) { - attachmentInfo.append(" ("); - if (!contentType.isEmpty()) { - attachmentInfo.append(escapeHtml(contentType)); - } - if (!encoding.isEmpty()) { - if (!contentType.isEmpty()) attachmentInfo.append(", "); - attachmentInfo.append("encoding: ").append(escapeHtml(encoding)); - } - attachmentInfo.append(")"); - } - attachmentInfo.append("
\n"); - } - - private static boolean isInvalidEmlFormat(byte[] emlBytes) { - try { - int checkLength = Math.min(emlBytes.length, StyleConstants.EML_CHECK_LENGTH); - String content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8); - String lowerContent = content.toLowerCase(); - - boolean hasFrom = - lowerContent.contains("from:") || lowerContent.contains("return-path:"); - boolean hasSubject = lowerContent.contains("subject:"); - boolean hasMessageId = lowerContent.contains("message-id:"); - boolean hasDate = lowerContent.contains("date:"); - boolean hasTo = - lowerContent.contains("to:") - || lowerContent.contains("cc:") - || lowerContent.contains("bcc:"); - boolean hasMimeStructure = - lowerContent.contains("multipart/") - || lowerContent.contains("text/plain") - || lowerContent.contains("text/html") - || lowerContent.contains("boundary="); - - int headerCount = 0; - if (hasFrom) headerCount++; - if (hasSubject) headerCount++; - if (hasMessageId) headerCount++; - if (hasDate) headerCount++; - if (hasTo) headerCount++; - - return headerCount < StyleConstants.MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure; - - } catch (RuntimeException e) { - return false; - } - } - - private static String extractBasicHeader(String emlContent, String headerName) { - try { - String[] lines = emlContent.split("\r?\n"); - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; - if (line.toLowerCase().startsWith(headerName.toLowerCase())) { - StringBuilder value = - new StringBuilder(line.substring(headerName.length()).trim()); - // Handle multi-line headers - for (int j = i + 1; j < lines.length; j++) { - if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) { - value.append(" ").append(lines[j].trim()); - } else { - break; - } - } - // Apply MIME header decoding - return safeMimeDecode(value.toString()); - } - if (line.trim().isEmpty()) break; - } - } catch (RuntimeException e) { - log.warn("Error extracting header '{}': {}", headerName, e.getMessage()); - } - return ""; - } - - private static String extractHtmlBody(String emlContent) { - try { - String lowerContent = emlContent.toLowerCase(); - int htmlStart = lowerContent.indexOf("content-type: text/html"); - if (htmlStart == -1) return null; - - return getString(emlContent, htmlStart); - - } catch (Exception e) { - return null; - } - } - - @Nullable - private static String getString(String emlContent, int htmlStart) { - int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart); - if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart); - if (bodyStart == -1) return null; - - bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; - int bodyEnd = findPartEnd(emlContent, bodyStart); - - return emlContent.substring(bodyStart, bodyEnd).trim(); - } - - private static String extractTextBody(String emlContent) { - try { - String lowerContent = emlContent.toLowerCase(); - int textStart = lowerContent.indexOf("content-type: text/plain"); - if (textStart == -1) { - int bodyStart = emlContent.indexOf("\r\n\r\n"); - if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n"); - if (bodyStart != -1) { - bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; - int bodyEnd = findPartEnd(emlContent, bodyStart); - return emlContent.substring(bodyStart, bodyEnd).trim(); - } - return null; - } - - return getString(emlContent, textStart); - - } catch (RuntimeException e) { - return null; - } - } - - private static int findPartEnd(String content, int start) { - String[] lines = content.substring(start).split("\r?\n"); - StringBuilder result = new StringBuilder(); - - for (String line : lines) { - if (line.startsWith("--") && line.length() > 10) break; - result.append(line).append("\n"); - } - - return start + result.length(); - } - - private static String convertTextToHtml(String textBody) { - if (textBody == null) return ""; - - String html = escapeHtml(textBody); - html = html.replace("\r\n", "\n").replace("\r", "\n"); - html = html.replace("\n", "
\n"); - - html = - html.replaceAll( - "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", - "$1"); - - html = - html.replaceAll( - "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})", - "$1"); - - return html; - } - - private static String processEmailHtmlBody(String htmlBody) { - return processEmailHtmlBody(htmlBody, null); - } - - private static String processEmailHtmlBody(String htmlBody, EmailContent emailContent) { - if (htmlBody == null) return ""; - - String processed = htmlBody; - - // Remove problematic CSS - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", ""); - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", ""); - - // Process inline images (cid: references) if we have email content with attachments - if (emailContent != null && !emailContent.getAttachments().isEmpty()) { - processed = processInlineImages(processed, emailContent); - } - - return processed; - } - - private static String processInlineImages(String htmlContent, EmailContent emailContent) { - if (htmlContent == null || emailContent == null) return htmlContent; - - // Create a map of Content-ID to attachment data - Map contentIdMap = new HashMap<>(); - for (EmailAttachment attachment : emailContent.getAttachments()) { - if (attachment.isEmbedded() - && attachment.getContentId() != null - && attachment.getData() != null) { - contentIdMap.put(attachment.getContentId(), attachment); - } - } - - if (contentIdMap.isEmpty()) return htmlContent; - - // Pattern to match cid: references in img src attributes - Pattern cidPattern = - Pattern.compile( - "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", - Pattern.CASE_INSENSITIVE); - Matcher matcher = cidPattern.matcher(htmlContent); - - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String contentId = matcher.group(1); - EmailAttachment attachment = contentIdMap.get(contentId); - - if (attachment != null && attachment.getData() != null) { - // Convert to data URI - String mimeType = attachment.getContentType(); - if (mimeType == null || mimeType.isEmpty()) { - // Try to determine MIME type from filename - String filename = attachment.getFilename(); - if (filename != null) { - if (filename.toLowerCase().endsWith(".png")) { - mimeType = "image/png"; - } else if (filename.toLowerCase().endsWith(".jpg") - || filename.toLowerCase().endsWith(".jpeg")) { - mimeType = "image/jpeg"; - } else if (filename.toLowerCase().endsWith(".gif")) { - mimeType = "image/gif"; - } else if (filename.toLowerCase().endsWith(".bmp")) { - mimeType = "image/bmp"; - } else { - mimeType = "image/png"; // fallback - } - } else { - mimeType = "image/png"; // fallback - } - } - - String base64Data = Base64.getEncoder().encodeToString(attachment.getData()); - String dataUri = "data:" + mimeType + ";base64," + base64Data; - - // Replace the cid: reference with the data URI - String replacement = - matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri); - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } else { - // Keep original if attachment not found - matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0))); - } - } - matcher.appendTail(result); - - return result.toString(); - } - - private static void appendEnhancedStyles(StringBuilder html) { - int fontSize = StyleConstants.DEFAULT_FONT_SIZE; - String textColor = StyleConstants.DEFAULT_TEXT_COLOR; - String backgroundColor = StyleConstants.DEFAULT_BACKGROUND_COLOR; - String borderColor = StyleConstants.DEFAULT_BORDER_COLOR; - - html.append("body {\n"); - html.append(" font-family: ").append(StyleConstants.DEFAULT_FONT_FAMILY).append(";\n"); - html.append(" font-size: ").append(fontSize).append("px;\n"); - html.append(" line-height: ").append(StyleConstants.DEFAULT_LINE_HEIGHT).append(";\n"); - html.append(" color: ").append(textColor).append(";\n"); - html.append(" margin: 0;\n"); - html.append(" padding: 16px;\n"); - html.append(" background-color: ").append(backgroundColor).append(";\n"); - html.append("}\n\n"); - - html.append(".email-container {\n"); - html.append(" width: 100%;\n"); - html.append(" max-width: 100%;\n"); - html.append(" margin: 0 auto;\n"); - html.append("}\n\n"); - - html.append(".email-header {\n"); - html.append(" padding-bottom: 10px;\n"); - html.append(" border-bottom: 1px solid ").append(borderColor).append(";\n"); - html.append(" margin-bottom: 10px;\n"); - html.append("}\n\n"); - html.append(".email-header h1 {\n"); - html.append(" margin: 0 0 10px 0;\n"); - html.append(" font-size: ").append(fontSize + 4).append("px;\n"); - html.append(" font-weight: bold;\n"); - html.append("}\n\n"); - html.append(".email-meta div {\n"); - html.append(" margin-bottom: 2px;\n"); - html.append(" font-size: ").append(fontSize - 1).append("px;\n"); - html.append("}\n\n"); - - html.append(".email-body {\n"); - html.append(" word-wrap: break-word;\n"); - html.append("}\n\n"); - - html.append(".attachment-section {\n"); - html.append(" margin-top: 15px;\n"); - html.append(" padding: 10px;\n"); - html.append(" background-color: ") - .append(StyleConstants.ATTACHMENT_BACKGROUND_COLOR) - .append(";\n"); - html.append(" border: 1px solid ") - .append(StyleConstants.ATTACHMENT_BORDER_COLOR) - .append(";\n"); - html.append(" border-radius: 3px;\n"); - html.append("}\n\n"); - html.append(".attachment-section h3 {\n"); - html.append(" margin: 0 0 8px 0;\n"); - html.append(" font-size: ").append(fontSize + 1).append("px;\n"); - html.append("}\n\n"); - html.append(".attachment-item {\n"); - html.append(" padding: 5px 0;\n"); - html.append("}\n\n"); - html.append(".attachment-icon {\n"); - html.append(" margin-right: 5px;\n"); - html.append("}\n\n"); - html.append(".attachment-details, .attachment-type {\n"); - html.append(" font-size: ").append(fontSize - 2).append("px;\n"); - html.append(" color: #555555;\n"); - html.append("}\n\n"); - html.append(".attachment-inclusion-note, .attachment-info-note {\n"); - html.append(" margin-top: 8px;\n"); - html.append(" padding: 6px;\n"); - html.append(" font-size: ").append(fontSize - 2).append("px;\n"); - html.append(" border-radius: 3px;\n"); - html.append("}\n\n"); - html.append(".attachment-inclusion-note {\n"); - html.append(" background-color: #e6ffed;\n"); - html.append(" border: 1px solid #d4f7dc;\n"); - html.append(" color: #006420;\n"); - html.append("}\n\n"); - html.append(".attachment-info-note {\n"); - html.append(" background-color: #fff9e6;\n"); - html.append(" border: 1px solid #fff0c2;\n"); - html.append(" color: #664d00;\n"); - html.append("}\n\n"); - html.append(".attachment-link-container {\n"); - html.append(" display: flex;\n"); - html.append(" align-items: center;\n"); - html.append(" padding: 8px;\n"); - html.append(" background-color: #f8f9fa;\n"); - html.append(" border: 1px solid #dee2e6;\n"); - html.append(" border-radius: 4px;\n"); - html.append(" margin: 4px 0;\n"); - html.append("}\n\n"); - html.append(".attachment-link-container:hover {\n"); - html.append(" background-color: #e9ecef;\n"); - html.append("}\n\n"); - html.append(".attachment-note {\n"); - html.append(" font-size: ").append(fontSize - 3).append("px;\n"); - html.append(" color: #6c757d;\n"); - html.append(" font-style: italic;\n"); - html.append(" margin-left: 8px;\n"); - html.append("}\n\n"); - - // Basic image styling: ensure images are responsive but not overly constrained. - html.append("img {\n"); - html.append(" max-width: 100%;\n"); // Make images responsive to container width - html.append(" height: auto;\n"); // Maintain aspect ratio - html.append(" display: block;\n"); // Avoid extra space below images - html.append("}\n\n"); - } - - private static String escapeHtml(String text) { - if (text == null) return ""; - return text.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - - private static stirling.software.common.model.api.converters.HTMLToPdfRequest createHtmlRequest( - EmlToPdfRequest request) { - stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest = - new stirling.software.common.model.api.converters.HTMLToPdfRequest(); - - if (request != null) { - htmlRequest.setFileInput(request.getFileInput()); - } - - // Set default zoom level - htmlRequest.setZoom(Float.parseFloat(StyleConstants.DEFAULT_ZOOM)); - - return htmlRequest; - } - - private static EmailContent extractEmailContentAdvanced( - Object message, EmlToPdfRequest request) { - EmailContent content = new EmailContent(); - - try { - Class messageClass = message.getClass(); - - // Extract headers via reflection - Method getSubject = messageClass.getMethod("getSubject"); - String subject = (String) getSubject.invoke(message); - content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject"); - - Method getFrom = messageClass.getMethod("getFrom"); - Object[] fromAddresses = (Object[]) getFrom.invoke(message); - content.setFrom( - fromAddresses != null && fromAddresses.length > 0 - ? safeMimeDecode(fromAddresses[0].toString()) - : ""); - - Method getAllRecipients = messageClass.getMethod("getAllRecipients"); - Object[] recipients = (Object[]) getAllRecipients.invoke(message); - content.setTo( - recipients != null && recipients.length > 0 - ? safeMimeDecode(recipients[0].toString()) - : ""); - - Method getSentDate = messageClass.getMethod("getSentDate"); - content.setDate((Date) getSentDate.invoke(message)); - - // Extract content - Method getContent = messageClass.getMethod("getContent"); - Object messageContent = getContent.invoke(message); - - if (messageContent instanceof String stringContent) { - Method getContentType = messageClass.getMethod("getContentType"); - String contentType = (String) getContentType.invoke(message); - if (contentType != null && contentType.toLowerCase().contains("text/html")) { - content.setHtmlBody(stringContent); - } else { - content.setTextBody(stringContent); - } - } else { - // Handle multipart content - try { - Class multipartClass = Class.forName("jakarta.mail.Multipart"); - if (multipartClass.isInstance(messageContent)) { - processMultipartAdvanced(messageContent, content, request); - } - } catch (Exception e) { - log.warn("Error processing content: {}", e.getMessage()); - } - } - - } catch (Exception e) { - content.setSubject("Email Conversion"); - content.setFrom("Unknown"); - content.setTo("Unknown"); - content.setTextBody("Email content could not be parsed with advanced processing"); - } - - return content; - } - - private static void processMultipartAdvanced( - Object multipart, EmailContent content, EmlToPdfRequest request) { - try { - // Enhanced multipart type checking - if (!isValidJakartaMailMultipart(multipart)) { - log.warn("Invalid Jakarta Mail multipart type: {}", multipart.getClass().getName()); - return; - } - - Class multipartClass = multipart.getClass(); - Method getCount = multipartClass.getMethod("getCount"); - int count = (Integer) getCount.invoke(multipart); - - Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class); - - for (int i = 0; i < count; i++) { - Object part = getBodyPart.invoke(multipart, i); - processPartAdvanced(part, content, request); - } - - } catch (Exception e) { - content.setTextBody("Email content could not be parsed with advanced processing"); - } - } - - private static void processPartAdvanced( - Object part, EmailContent content, EmlToPdfRequest request) { - try { - if (!isValidJakartaMailPart(part)) { - log.warn("Invalid Jakarta Mail part type: {}", part.getClass().getName()); - return; - } - - Class partClass = part.getClass(); - Method isMimeType = partClass.getMethod("isMimeType", String.class); - Method getContent = partClass.getMethod("getContent"); - Method getDisposition = partClass.getMethod("getDisposition"); - Method getFileName = partClass.getMethod("getFileName"); - Method getContentType = partClass.getMethod("getContentType"); - Method getHeader = partClass.getMethod("getHeader", String.class); - - Object disposition = getDisposition.invoke(part); - String filename = (String) getFileName.invoke(part); - String contentType = (String) getContentType.invoke(part); - - if ((Boolean) isMimeType.invoke(part, "text/plain") && disposition == null) { - content.setTextBody((String) getContent.invoke(part)); - } else if ((Boolean) isMimeType.invoke(part, "text/html") && disposition == null) { - content.setHtmlBody((String) getContent.invoke(part)); - } else if ("attachment".equalsIgnoreCase((String) disposition) - || (filename != null && !filename.trim().isEmpty())) { - - content.setAttachmentCount(content.getAttachmentCount() + 1); - - // Always extract basic attachment metadata for display - if (filename != null && !filename.trim().isEmpty()) { - // Create attachment with metadata only - EmailAttachment attachment = new EmailAttachment(); - // Apply MIME decoding to filename to handle encoded attachment names - attachment.setFilename(safeMimeDecode(filename)); - attachment.setContentType(contentType); - - // Check if it's an embedded image - String[] contentIdHeaders = (String[]) getHeader.invoke(part, "Content-ID"); - if (contentIdHeaders != null && contentIdHeaders.length > 0) { - attachment.setEmbedded(true); - // Store the Content-ID, removing angle brackets if present - String contentId = contentIdHeaders[0]; - if (contentId.startsWith("<") && contentId.endsWith(">")) { - contentId = contentId.substring(1, contentId.length() - 1); - } - attachment.setContentId(contentId); - } - - // Extract attachment data if attachments should be included OR if it's an - // embedded image (needed for inline display) - if ((request != null && request.isIncludeAttachments()) - || attachment.isEmbedded()) { - try { - Object attachmentContent = getContent.invoke(part); - byte[] attachmentData = null; - - if (attachmentContent instanceof java.io.InputStream inputStream) { - try { - attachmentData = inputStream.readAllBytes(); - } catch (IOException e) { - log.warn( - "Failed to read InputStream attachment: {}", - e.getMessage()); - } - } else if (attachmentContent instanceof byte[] byteArray) { - attachmentData = byteArray; - } else if (attachmentContent instanceof String stringContent) { - attachmentData = stringContent.getBytes(StandardCharsets.UTF_8); - } - - if (attachmentData != null) { - // Check size limit (use default 10MB if request is null) - long maxSizeMB = - request != null ? request.getMaxAttachmentSizeMB() : 10L; - long maxSizeBytes = maxSizeMB * 1024 * 1024; - - if (attachmentData.length <= maxSizeBytes) { - attachment.setData(attachmentData); - attachment.setSizeBytes(attachmentData.length); - } else { - // For embedded images, always include data regardless of size - // to ensure inline display works - if (attachment.isEmbedded()) { - attachment.setData(attachmentData); - attachment.setSizeBytes(attachmentData.length); - } else { - // Still show attachment info even if too large - attachment.setSizeBytes(attachmentData.length); - } - } - } - } catch (Exception e) { - log.warn("Error extracting attachment data: {}", e.getMessage()); - } - } - - // Add attachment to the list for display (with or without data) - content.getAttachments().add(attachment); - } - } else if ((Boolean) isMimeType.invoke(part, "multipart/*")) { - // Handle nested multipart content - try { - Object multipartContent = getContent.invoke(part); - Class multipartClass = Class.forName("jakarta.mail.Multipart"); - if (multipartClass.isInstance(multipartContent)) { - processMultipartAdvanced(multipartContent, content, request); - } - } catch (Exception e) { - log.warn("Error processing multipart content: {}", e.getMessage()); - } - } - - } catch (Exception e) { - log.warn("Error processing multipart part: {}", e.getMessage()); - } - } - - private static String generateEnhancedEmailHtml(EmailContent content, EmlToPdfRequest request) { - StringBuilder html = new StringBuilder(); - - html.append("\n"); - html.append("\n"); - html.append("").append(escapeHtml(content.getSubject())).append("\n"); - html.append("\n"); - html.append("\n"); - - html.append("
\n"); - html.append("
\n"); - html.append("

").append(escapeHtml(content.getSubject())).append("

\n"); - html.append("
\n"); - html.append("
From: ") - .append(escapeHtml(content.getFrom())) - .append("
\n"); - html.append("
To: ") - .append(escapeHtml(content.getTo())) - .append("
\n"); - - if (content.getDate() != null) { - html.append("
Date: ") - .append(formatEmailDate(content.getDate())) - .append("
\n"); - } - html.append("
\n"); - - html.append("
\n"); - if (content.getHtmlBody() != null && !content.getHtmlBody().trim().isEmpty()) { - html.append(processEmailHtmlBody(content.getHtmlBody(), content)); - } else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) { - html.append("
"); - html.append(convertTextToHtml(content.getTextBody())); - html.append("
"); - } else { - html.append("
"); - html.append("

No content available

"); - html.append("
"); - } - html.append("
\n"); - - if (content.getAttachmentCount() > 0 || !content.getAttachments().isEmpty()) { - html.append("
\n"); - int displayedAttachmentCount = - content.getAttachmentCount() > 0 - ? content.getAttachmentCount() - : content.getAttachments().size(); - html.append("

Attachments (").append(displayedAttachmentCount).append(")

\n"); - - if (!content.getAttachments().isEmpty()) { - for (EmailAttachment attachment : content.getAttachments()) { - // Create attachment info with paperclip emoji before filename - String uniqueId = generateUniqueAttachmentId(attachment.getFilename()); - attachment.setEmbeddedFilename( - attachment.getEmbeddedFilename() != null - ? attachment.getEmbeddedFilename() - : attachment.getFilename()); - - html.append("
") - .append("") - .append(MimeConstants.ATTACHMENT_MARKER) - .append(" ") - .append("") - .append(escapeHtml(safeMimeDecode(attachment.getFilename()))) - .append(""); - - String sizeStr = formatFileSize(attachment.getSizeBytes()); - html.append(" (").append(sizeStr); - if (attachment.getContentType() != null - && !attachment.getContentType().isEmpty()) { - html.append(", ").append(escapeHtml(attachment.getContentType())); - } - html.append(")
\n"); - } - } - - if (request.isIncludeAttachments()) { - html.append("
\n"); - html.append("

Attachments are embedded in the file.

\n"); - html.append("
\n"); - } else { - html.append("
\n"); - html.append( - "

Attachment information displayed - files not included in PDF.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - } - - html.append("
\n"); - html.append(""); - - return html.toString(); - } - - private static byte[] attachFilesToPdf( - byte[] pdfBytes, - List attachments, - CustomPDFDocumentFactory pdfDocumentFactory) - throws IOException { - try (PDDocument document = pdfDocumentFactory.load(pdfBytes); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - - if (attachments == null || attachments.isEmpty()) { - document.save(outputStream); - return outputStream.toByteArray(); - } - - List embeddedFiles = new ArrayList<>(); - - // Set up the embedded files name tree once - if (document.getDocumentCatalog().getNames() == null) { - document.getDocumentCatalog() - .setNames(new PDDocumentNameDictionary(document.getDocumentCatalog())); - } - - PDDocumentNameDictionary names = document.getDocumentCatalog().getNames(); - if (names.getEmbeddedFiles() == null) { - names.setEmbeddedFiles(new PDEmbeddedFilesNameTreeNode()); - } - - PDEmbeddedFilesNameTreeNode efTree = names.getEmbeddedFiles(); - Map efMap = efTree.getNames(); - if (efMap == null) { - efMap = new HashMap<>(); - } - - // Embed each attachment directly into the PDF - for (EmailAttachment attachment : attachments) { - if (attachment.getData() == null || attachment.getData().length == 0) { - continue; - } - - try { - // Generate unique filename - String filename = attachment.getFilename(); - if (filename == null || filename.trim().isEmpty()) { - filename = "attachment_" + System.currentTimeMillis(); - if (attachment.getContentType() != null - && attachment.getContentType().contains("/")) { - String[] parts = attachment.getContentType().split("/"); - if (parts.length > 1) { - filename += "." + parts[1]; - } - } - } - - // Ensure unique filename - String uniqueFilename = getUniqueFilename(filename, embeddedFiles, efMap); - - // Create embedded file - PDEmbeddedFile embeddedFile = - new PDEmbeddedFile( - document, new ByteArrayInputStream(attachment.getData())); - embeddedFile.setSize(attachment.getData().length); - embeddedFile.setCreationDate(new GregorianCalendar()); - - // Create file specification - PDComplexFileSpecification fileSpec = new PDComplexFileSpecification(); - fileSpec.setFile(uniqueFilename); - fileSpec.setEmbeddedFile(embeddedFile); - if (attachment.getContentType() != null) { - embeddedFile.setSubtype(attachment.getContentType()); - fileSpec.setFileDescription("Email attachment: " + uniqueFilename); - } - - // Add to the map (but don't set it yet) - efMap.put(uniqueFilename, fileSpec); - embeddedFiles.add(uniqueFilename); - - // Store the filename for annotation creation - attachment.setEmbeddedFilename(uniqueFilename); - - } catch (Exception e) { - // Log error but continue with other attachments - log.warn("Failed to embed attachment: {}", attachment.getFilename(), e); - } - } - - // Set the complete map once at the end - if (!efMap.isEmpty()) { - efTree.setNames(efMap); - - // Set catalog viewer preferences to automatically show attachments pane - setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS); - } - - // Add attachment annotations to the first page for each embedded file - if (!embeddedFiles.isEmpty()) { - addAttachmentAnnotationsToDocument(document, attachments); - } - - document.save(outputStream); - return outputStream.toByteArray(); - } - } - - private static String getUniqueFilename( - String filename, - List embeddedFiles, - Map efMap) { - String uniqueFilename = filename; - int counter = 1; - while (embeddedFiles.contains(uniqueFilename) || efMap.containsKey(uniqueFilename)) { - String extension = ""; - String baseName = filename; - int lastDot = filename.lastIndexOf('.'); - if (lastDot > 0) { - extension = filename.substring(lastDot); - baseName = filename.substring(0, lastDot); - } - uniqueFilename = baseName + "_" + counter + extension; - counter++; - } - return uniqueFilename; - } - - private static void addAttachmentAnnotationsToDocument( - PDDocument document, List attachments) throws IOException { - if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) { - return; - } - - // 1. Find the screen position of all attachment markers - AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder(); - finder.setSortByPosition(true); // Process pages in order - finder.getText(document); - List markerPositions = finder.getPositions(); - - // 2. Warn if the number of markers and attachments don't match - if (markerPositions.size() != attachments.size()) { - log.warn( - "Found {} attachment markers, but there are {} attachments. Annotation count may be incorrect.", - markerPositions.size(), - attachments.size()); - } - - // 3. Create an invisible annotation over each found marker - int annotationsToAdd = Math.min(markerPositions.size(), attachments.size()); - for (int i = 0; i < annotationsToAdd; i++) { - MarkerPosition position = markerPositions.get(i); - EmailAttachment attachment = attachments.get(i); - - if (attachment.getEmbeddedFilename() != null) { - PDPage page = document.getPage(position.getPageIndex()); - addAttachmentAnnotationToPage( - document, page, attachment, position.getX(), position.getY()); - } - } - } - - private static void addAttachmentAnnotationToPage( - PDDocument document, PDPage page, EmailAttachment attachment, float x, float y) - throws IOException { - - PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment(); - - PDRectangle rect = getPdRectangle(page, x, y); - fileAnnotation.setRectangle(rect); - - // Remove visual appearance while keeping clickable functionality - try { - PDAppearanceDictionary appearance = new PDAppearanceDictionary(); - PDAppearanceStream normalAppearance = new PDAppearanceStream(document); - normalAppearance.setBBox(new PDRectangle(0, 0, 0, 0)); // Zero-size bounding box - - appearance.setNormalAppearance(normalAppearance); - fileAnnotation.setAppearance(appearance); - } catch (Exception e) { - // If appearance manipulation fails, just set it to null - fileAnnotation.setAppearance(null); - } - - // Set invisibility flags but keep it functional - fileAnnotation.setInvisible(true); - fileAnnotation.setHidden(false); // Must be false to remain clickable - fileAnnotation.setNoView(false); // Must be false to remain clickable - fileAnnotation.setPrinted(false); - - PDEmbeddedFilesNameTreeNode efTree = - document.getDocumentCatalog().getNames().getEmbeddedFiles(); - if (efTree != null) { - Map efMap = efTree.getNames(); - if (efMap != null) { - PDComplexFileSpecification fileSpec = efMap.get(attachment.getEmbeddedFilename()); - if (fileSpec != null) { - fileAnnotation.setFile(fileSpec); - } - } - } - - fileAnnotation.setContents("Click to open: " + attachment.getFilename()); - fileAnnotation.setAnnotationName("EmbeddedFile_" + attachment.getEmbeddedFilename()); - - page.getAnnotations().add(fileAnnotation); - - log.info( - "Added attachment annotation for '{}' on page {}", - attachment.getFilename(), - document.getPages().indexOf(page) + 1); - } - - private static @NotNull PDRectangle getPdRectangle(PDPage page, float x, float y) { - PDRectangle mediaBox = page.getMediaBox(); - float pdfY = mediaBox.getHeight() - y; - - float iconWidth = - StyleConstants.ATTACHMENT_ICON_WIDTH; // Keep original size for clickability - float iconHeight = - StyleConstants.ATTACHMENT_ICON_HEIGHT; // Keep original size for clickability - - // Keep the full-size rectangle so it remains clickable - return new PDRectangle( - x + StyleConstants.ANNOTATION_X_OFFSET, - pdfY - iconHeight + StyleConstants.ANNOTATION_Y_OFFSET, - iconWidth, - iconHeight); - } - - private static String formatEmailDate(Date date) { - if (date == null) return ""; - java.text.SimpleDateFormat formatter = - new java.text.SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a", Locale.ENGLISH); - return formatter.format(date); - } - - private static String formatFileSize(long bytes) { - if (bytes < FileSizeConstants.BYTES_IN_KB) { - return bytes + " B"; - } else if (bytes < FileSizeConstants.BYTES_IN_MB) { - return String.format("%.1f KB", bytes / (double) FileSizeConstants.BYTES_IN_KB); - } else if (bytes < FileSizeConstants.BYTES_IN_GB) { - return String.format("%.1f MB", bytes / (double) FileSizeConstants.BYTES_IN_MB); - } else { - return String.format("%.1f GB", bytes / (double) FileSizeConstants.BYTES_IN_GB); - } - } - - // MIME header decoding functionality for RFC 2047 encoded headers - moved to constants - - private static String decodeMimeHeader(String encodedText) { - if (encodedText == null || encodedText.trim().isEmpty()) { - return encodedText; - } - - try { - StringBuilder result = new StringBuilder(); - Matcher matcher = MimeConstants.MIME_ENCODED_PATTERN.matcher(encodedText); - int lastEnd = 0; - - while (matcher.find()) { - // Add any text before the encoded part - result.append(encodedText, lastEnd, matcher.start()); - - String charset = matcher.group(1); - String encoding = matcher.group(2).toUpperCase(); - String encodedValue = matcher.group(3); - - try { - String decodedValue; - if ("B".equals(encoding)) { - // Base64 decoding - byte[] decodedBytes = Base64.getDecoder().decode(encodedValue); - decodedValue = new String(decodedBytes, Charset.forName(charset)); - } else if ("Q".equals(encoding)) { - // Quoted-printable decoding - decodedValue = decodeQuotedPrintable(encodedValue, charset); - } else { - // Unknown encoding, keep original - decodedValue = matcher.group(0); - } - result.append(decodedValue); - } catch (Exception e) { - log.warn("Failed to decode MIME header part: {}", matcher.group(0), e); - // If decoding fails, keep the original encoded text - result.append(matcher.group(0)); - } - - lastEnd = matcher.end(); - } - - // Add any remaining text after the last encoded part - result.append(encodedText.substring(lastEnd)); - - return result.toString(); - } catch (Exception e) { - log.warn("Error decoding MIME header: {}", encodedText, e); - return encodedText; // Return original if decoding fails - } - } - - private static String decodeQuotedPrintable(String encodedText, String charset) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < encodedText.length(); i++) { - char c = encodedText.charAt(i); - switch (c) { - case '=' -> { - if (i + 2 < encodedText.length()) { - String hex = encodedText.substring(i + 1, i + 3); - try { - int value = Integer.parseInt(hex, 16); - result.append((char) value); - i += 2; // Skip the hex digits - } catch (NumberFormatException e) { - // If hex parsing fails, keep the original character - result.append(c); - } - } else { - result.append(c); - } - } - case '_' -> // In RFC 2047, underscore represents space - result.append(' '); - default -> result.append(c); - } - } - - // Convert bytes to proper charset - byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1); - return new String(bytes, Charset.forName(charset)); - } - - private static String safeMimeDecode(String headerValue) { - if (headerValue == null) { - return ""; - } - - try { - if (isJakartaMailAvailable()) { - // Use Jakarta Mail's MimeUtility for proper MIME decoding - Class mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility"); - Method decodeText = mimeUtilityClass.getMethod("decodeText", String.class); - return (String) decodeText.invoke(null, headerValue.trim()); - } else { - // Fallback to basic MIME decoding - return decodeMimeHeader(headerValue.trim()); - } - } catch (Exception e) { - log.warn("Failed to decode MIME header, using original: {}", headerValue, e); - return headerValue; - } - } - - private static boolean isValidJakartaMailPart(Object part) { - if (part == null) return false; - - try { - // Check if the object implements jakarta.mail.Part interface - Class partInterface = Class.forName("jakarta.mail.Part"); - if (!partInterface.isInstance(part)) { - return false; - } - - // Additional check for MimePart - try { - Class mimePartInterface = Class.forName("jakarta.mail.internet.MimePart"); - return mimePartInterface.isInstance(part); - } catch (ClassNotFoundException e) { - // MimePart not available, but Part is sufficient - return true; - } - } catch (ClassNotFoundException e) { - log.debug("Jakarta Mail Part interface not available for validation"); - return false; - } - } - - private static boolean isValidJakartaMailMultipart(Object multipart) { - if (multipart == null) return false; - - try { - // Check if the object implements jakarta.mail.Multipart interface - Class multipartInterface = Class.forName("jakarta.mail.Multipart"); - if (!multipartInterface.isInstance(multipart)) { - return false; - } - - // Additional check for MimeMultipart - try { - Class mimeMultipartClass = Class.forName("jakarta.mail.internet.MimeMultipart"); - if (mimeMultipartClass.isInstance(multipart)) { - log.debug("Found MimeMultipart instance for enhanced processing"); - return true; - } - } catch (ClassNotFoundException e) { - log.debug("MimeMultipart not available, using base Multipart interface"); - } - - return true; - } catch (ClassNotFoundException e) { - log.debug("Jakarta Mail Multipart interface not available for validation"); - return false; - } - } - - @Data - public static class EmailContent { - private String subject; - private String from; - private String to; - private Date date; - private String htmlBody; - private String textBody; - private int attachmentCount; - private List attachments = new ArrayList<>(); - - public void setHtmlBody(String htmlBody) { - this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null; - } - - public void setTextBody(String textBody) { - this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null; - } - } - - @Data - public static class EmailAttachment { - private String filename; - private String contentType; - private byte[] data; - private boolean embedded; - private String embeddedFilename; - private long sizeBytes; - - // New fields for advanced processing - private String contentId; - private String disposition; - private String transferEncoding; - - // Custom setter to maintain size calculation logic - public void setData(byte[] data) { - this.data = data; - if (data != null) { - this.sizeBytes = data.length; - } - } - } - - @Data - public static class MarkerPosition { - private int pageIndex; - private float x; - private float y; - private String character; - - public MarkerPosition(int pageIndex, float x, float y, String character) { - this.pageIndex = pageIndex; - this.x = x; - this.y = y; - this.character = character; - } - } - - public static class AttachmentMarkerPositionFinder - extends org.apache.pdfbox.text.PDFTextStripper { - @Getter private final List positions = new ArrayList<>(); - private int currentPageIndex; - protected boolean sortByPosition; - private boolean isInAttachmentSection; - private boolean attachmentSectionFound; - - public AttachmentMarkerPositionFinder() { - super(); - this.currentPageIndex = 0; - this.sortByPosition = false; - this.isInAttachmentSection = false; - this.attachmentSectionFound = false; - } - - @Override - protected void startPage(org.apache.pdfbox.pdmodel.PDPage page) throws IOException { - super.startPage(page); - } - - @Override - protected void endPage(org.apache.pdfbox.pdmodel.PDPage page) throws IOException { - currentPageIndex++; - super.endPage(page); - } - - @Override - protected void writeString( - String string, List textPositions) - throws IOException { - // Check if we are entering or exiting the attachment section - String lowerString = string.toLowerCase(); - - // Look for attachment section start marker - if (lowerString.contains("attachments (")) { - isInAttachmentSection = true; - attachmentSectionFound = true; - } - - // Look for attachment section end markers (common patterns that indicate end of - // attachments) - if (isInAttachmentSection - && (lowerString.contains("") - || lowerString.contains("") - || (attachmentSectionFound - && lowerString.trim().isEmpty() - && string.length() > 50))) { - isInAttachmentSection = false; - } - - // Only look for markers if we are in the attachment section - if (isInAttachmentSection) { - String attachmentMarker = MimeConstants.ATTACHMENT_MARKER; - for (int i = 0; (i = string.indexOf(attachmentMarker, i)) != -1; i++) { - if (i < textPositions.size()) { - org.apache.pdfbox.text.TextPosition textPosition = textPositions.get(i); - MarkerPosition position = - new MarkerPosition( - currentPageIndex, - textPosition.getXDirAdj(), - textPosition.getYDirAdj(), - attachmentMarker); - positions.add(position); - } - } - } - super.writeString(string, textPositions); - } - - @Override - public void setSortByPosition(boolean sortByPosition) { - this.sortByPosition = sortByPosition; - } - } } diff --git a/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java new file mode 100644 index 000000000..2478aad94 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java @@ -0,0 +1,680 @@ +package stirling.software.common.util; + +import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PageMode; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.jetbrains.annotations.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import lombok.Data; +import lombok.Getter; +import lombok.experimental.UtilityClass; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@UtilityClass +public class PdfAttachmentHandler { + // Note: This class is designed for EML attachments, not general PDF attachments. + + private static final String ATTACHMENT_MARKER = "@"; + private static final float ATTACHMENT_ICON_WIDTH = 12f; + private static final float ATTACHMENT_ICON_HEIGHT = 14f; + private static final float ANNOTATION_X_OFFSET = 2f; + private static final float ANNOTATION_Y_OFFSET = 10f; + + public static byte[] attachFilesToPdf( + byte[] pdfBytes, + List attachments, + CustomPDFDocumentFactory pdfDocumentFactory) + throws IOException { + + if (attachments == null || attachments.isEmpty()) { + return pdfBytes; + } + + try (PDDocument document = pdfDocumentFactory.load(pdfBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + List multipartAttachments = new ArrayList<>(attachments.size()); + for (int i = 0; i < attachments.size(); i++) { + EmlParser.EmailAttachment attachment = attachments.get(i); + if (attachment.getData() != null && attachment.getData().length > 0) { + String embeddedFilename = + attachment.getFilename() != null + ? attachment.getFilename() + : ("attachment_" + i); + attachment.setEmbeddedFilename(embeddedFilename); + multipartAttachments.add(createMultipartFile(attachment)); + } + } + + if (!multipartAttachments.isEmpty()) { + Map indexToFilenameMap = + addAttachmentsToDocumentWithMapping( + document, multipartAttachments, attachments); + setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS); + addAttachmentAnnotationsToDocumentWithMapping( + document, attachments, indexToFilenameMap); + } + + document.save(outputStream); + return outputStream.toByteArray(); + } catch (RuntimeException e) { + throw new IOException( + "Invalid PDF structure or processing error: " + e.getMessage(), e); + } catch (Exception e) { + throw new IOException("Error attaching files to PDF: " + e.getMessage(), e); + } + } + + private static MultipartFile createMultipartFile(EmlParser.EmailAttachment attachment) { + return new MultipartFile() { + @Override + public @NotNull String getName() { + return "attachment"; + } + + @Override + public String getOriginalFilename() { + return attachment.getFilename() != null + ? attachment.getFilename() + : "attachment_" + System.currentTimeMillis(); + } + + @Override + public String getContentType() { + return attachment.getContentType() != null + ? attachment.getContentType() + : "application/octet-stream"; + } + + @Override + public boolean isEmpty() { + return attachment.getData() == null || attachment.getData().length == 0; + } + + @Override + public long getSize() { + return attachment.getData() != null ? attachment.getData().length : 0; + } + + @Override + public byte @NotNull [] getBytes() { + return attachment.getData() != null ? attachment.getData() : new byte[0]; + } + + @Override + public @NotNull InputStream getInputStream() { + byte[] data = attachment.getData(); + return new ByteArrayInputStream(data != null ? data : new byte[0]); + } + + @Override + public void transferTo(@NotNull File dest) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(dest)) { + byte[] data = attachment.getData(); + if (data != null) { + fos.write(data); + } + } + } + }; + } + + private static String ensureUniqueFilename(String filename, Set existingNames) { + if (!existingNames.contains(filename)) { + return filename; + } + + String baseName; + String extension = ""; + int lastDot = filename.lastIndexOf('.'); + if (lastDot > 0) { + baseName = filename.substring(0, lastDot); + extension = filename.substring(lastDot); + } else { + baseName = filename; + } + + int counter = 1; + String uniqueName; + do { + uniqueName = baseName + "_" + counter + extension; + counter++; + } while (existingNames.contains(uniqueName)); + + return uniqueName; + } + + private static @NotNull PDRectangle calculateAnnotationRectangle( + PDPage page, float x, float y) { + PDRectangle cropBox = page.getCropBox(); + + // ISO 32000-1:2008 Section 8.3: PDF coordinate system transforms + int rotation = page.getRotation(); + float pdfX = x; + float pdfY = cropBox.getHeight() - y; + + switch (rotation) { + case 90 -> { + float temp = pdfX; + pdfX = pdfY; + pdfY = cropBox.getWidth() - temp; + } + case 180 -> { + pdfX = cropBox.getWidth() - pdfX; + pdfY = y; + } + case 270 -> { + float temp = pdfX; + pdfX = cropBox.getHeight() - pdfY; + pdfY = temp; + } + default -> {} + } + + float iconHeight = ATTACHMENT_ICON_HEIGHT; + float paddingX = 2.0f; + float paddingY = 2.0f; + + PDRectangle rect = + new PDRectangle( + pdfX + ANNOTATION_X_OFFSET + paddingX, + pdfY - iconHeight + ANNOTATION_Y_OFFSET + paddingY, + ATTACHMENT_ICON_WIDTH, + iconHeight); + + PDRectangle mediaBox = page.getMediaBox(); + if (rect.getLowerLeftX() < mediaBox.getLowerLeftX() + || rect.getLowerLeftY() < mediaBox.getLowerLeftY() + || rect.getUpperRightX() > mediaBox.getUpperRightX() + || rect.getUpperRightY() > mediaBox.getUpperRightY()) { + + float adjustedX = + Math.max( + mediaBox.getLowerLeftX(), + Math.min( + rect.getLowerLeftX(), + mediaBox.getUpperRightX() - rect.getWidth())); + float adjustedY = + Math.max( + mediaBox.getLowerLeftY(), + Math.min( + rect.getLowerLeftY(), + mediaBox.getUpperRightY() - rect.getHeight())); + rect = new PDRectangle(adjustedX, adjustedY, rect.getWidth(), rect.getHeight()); + } + + return rect; + } + + public static String processInlineImages( + String htmlContent, EmlParser.EmailContent emailContent) { + if (htmlContent == null || emailContent == null) return htmlContent; + + Map contentIdMap = new HashMap<>(); + for (EmlParser.EmailAttachment attachment : emailContent.getAttachments()) { + if (attachment.isEmbedded() + && attachment.getContentId() != null + && attachment.getData() != null) { + contentIdMap.put(attachment.getContentId(), attachment); + } + } + + if (contentIdMap.isEmpty()) return htmlContent; + + Pattern cidPattern = + Pattern.compile( + "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", + Pattern.CASE_INSENSITIVE); + Matcher matcher = cidPattern.matcher(htmlContent); + + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String contentId = matcher.group(1); + EmlParser.EmailAttachment attachment = contentIdMap.get(contentId); + + if (attachment != null && attachment.getData() != null) { + String mimeType = + EmlProcessingUtils.detectMimeType( + attachment.getFilename(), attachment.getContentType()); + + String base64Data = Base64.getEncoder().encodeToString(attachment.getData()); + String dataUri = "data:" + mimeType + ";base64," + base64Data; + + String replacement = + matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } else { + matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0))); + } + } + matcher.appendTail(result); + + return result.toString(); + } + + public static String formatEmailDate(Date date) { + if (date == null) return ""; + + SimpleDateFormat formatter = + new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + return formatter.format(date); + } + + @Data + public static class MarkerPosition { + private int pageIndex; + private float x; + private float y; + private String character; + private String filename; + + public MarkerPosition(int pageIndex, float x, float y, String character, String filename) { + this.pageIndex = pageIndex; + this.x = x; + this.y = y; + this.character = character; + this.filename = filename; + } + } + + public static class AttachmentMarkerPositionFinder extends PDFTextStripper { + @Getter private final List positions = new ArrayList<>(); + private int currentPageIndex; + protected boolean sortByPosition; + private boolean isInAttachmentSection; + private boolean attachmentSectionFound; + private final StringBuilder currentText = new StringBuilder(); + + private static final Pattern ATTACHMENT_SECTION_PATTERN = + Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE); + + private static final Pattern FILENAME_PATTERN = + Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)"); + + public AttachmentMarkerPositionFinder() { + super(); + this.currentPageIndex = 0; + this.sortByPosition = false; // Disable sorting to preserve document order + this.isInAttachmentSection = false; + this.attachmentSectionFound = false; + } + + @Override + public String getText(PDDocument document) throws IOException { + super.getText(document); + + if (sortByPosition) { + positions.sort( + (a, b) -> { + int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex()); + if (pageCompare != 0) return pageCompare; + return Float.compare( + b.getY(), a.getY()); // Descending Y per PDF coordinate system + }); + } + + return ""; // Return empty string as we only need positions + } + + @Override + protected void startPage(PDPage page) throws IOException { + super.startPage(page); + } + + @Override + protected void endPage(PDPage page) throws IOException { + currentPageIndex++; + super.endPage(page); + } + + @Override + protected void writeString(String string, List textPositions) + throws IOException { + String lowerString = string.toLowerCase(); + + if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) { + isInAttachmentSection = true; + attachmentSectionFound = true; + } + + if (isInAttachmentSection + && (lowerString.contains("") + || lowerString.contains("") + || (attachmentSectionFound + && lowerString.trim().isEmpty() + && string.length() > 50))) { + isInAttachmentSection = false; + } + + if (isInAttachmentSection) { + currentText.append(string); + + for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) { + if (i < textPositions.size()) { + TextPosition textPosition = textPositions.get(i); + + String filename = extractFilenameAfterMarker(string, i); + + MarkerPosition position = + new MarkerPosition( + currentPageIndex, + textPosition.getXDirAdj(), + textPosition.getYDirAdj(), + ATTACHMENT_MARKER, + filename); + positions.add(position); + } + } + } + super.writeString(string, textPositions); + } + + @Override + public void setSortByPosition(boolean sortByPosition) { + this.sortByPosition = sortByPosition; + } + + private String extractFilenameAfterMarker(String text, int markerIndex) { + String afterMarker = text.substring(markerIndex + 1); + + Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker); + if (matcher.find()) { + return matcher.group(1); + } + + String[] parts = afterMarker.split("[\\s\\(\\)]+"); + for (String part : parts) { + part = part.trim(); + if (part.length() > 3 && part.contains(".")) { + return part; + } + } + + return null; + } + } + + private static Map addAttachmentsToDocumentWithMapping( + PDDocument document, + List attachments, + List originalAttachments) + throws IOException { + + PDDocumentCatalog catalog = document.getDocumentCatalog(); + + if (catalog == null) { + throw new IOException("PDF document catalog is not accessible"); + } + + PDDocumentNameDictionary documentNames = catalog.getNames(); + if (documentNames == null) { + documentNames = new PDDocumentNameDictionary(catalog); + catalog.setNames(documentNames); + } + + PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles(); + if (embeddedFilesTree == null) { + embeddedFilesTree = new PDEmbeddedFilesNameTreeNode(); + documentNames.setEmbeddedFiles(embeddedFilesTree); + } + + Map existingNames = embeddedFilesTree.getNames(); + if (existingNames == null) { + existingNames = new HashMap<>(); + } + + Map indexToFilenameMap = new HashMap<>(); + + for (int i = 0; i < attachments.size(); i++) { + MultipartFile attachment = attachments.get(i); + String filename = attachment.getOriginalFilename(); + if (filename == null || filename.trim().isEmpty()) { + filename = "attachment_" + i; + } + + String normalizedFilename = + isAscii(filename) + ? filename + : java.text.Normalizer.normalize( + filename, java.text.Normalizer.Form.NFC); + String uniqueFilename = + ensureUniqueFilename(normalizedFilename, existingNames.keySet()); + + indexToFilenameMap.put(i, uniqueFilename); + + PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream()); + embeddedFile.setSize((int) attachment.getSize()); + + GregorianCalendar currentTime = new GregorianCalendar(); + embeddedFile.setCreationDate(currentTime); + embeddedFile.setModDate(currentTime); + + String contentType = attachment.getContentType(); + if (contentType != null && !contentType.trim().isEmpty()) { + embeddedFile.setSubtype(contentType); + } + + PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification(); + fileSpecification.setFile(uniqueFilename); + fileSpecification.setFileUnicode(uniqueFilename); + fileSpecification.setEmbeddedFile(embeddedFile); + fileSpecification.setEmbeddedFileUnicode(embeddedFile); + + existingNames.put(uniqueFilename, fileSpecification); + } + + embeddedFilesTree.setNames(existingNames); + documentNames.setEmbeddedFiles(embeddedFilesTree); + catalog.setNames(documentNames); + + return indexToFilenameMap; + } + + private static void addAttachmentAnnotationsToDocumentWithMapping( + PDDocument document, + List attachments, + Map indexToFilenameMap) + throws IOException { + + if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) { + return; + } + + AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder(); + finder.setSortByPosition(false); // Keep document order to maintain pairing + finder.getText(document); + List markerPositions = finder.getPositions(); + + int annotationsToAdd = Math.min(markerPositions.size(), attachments.size()); + + for (int i = 0; i < annotationsToAdd; i++) { + MarkerPosition position = markerPositions.get(i); + + String filenameNearMarker = position.getFilename(); + + EmlParser.EmailAttachment matchingAttachment = + findAttachmentByFilename(attachments, filenameNearMarker); + + if (matchingAttachment != null) { + String embeddedFilename = + findEmbeddedFilenameForAttachment(matchingAttachment, indexToFilenameMap); + + if (embeddedFilename != null) { + PDPage page = document.getPage(position.getPageIndex()); + addAttachmentAnnotationToPageWithMapping( + document, + page, + matchingAttachment, + embeddedFilename, + position.getX(), + position.getY(), + i); + } else { + // No embedded filename found for attachment + } + } else { + // No matching attachment found for filename near marker + } + } + } + + private static EmlParser.EmailAttachment findAttachmentByFilename( + List attachments, String targetFilename) { + if (targetFilename == null || targetFilename.trim().isEmpty()) { + return null; + } + + String normalizedTarget = normalizeFilename(targetFilename); + + // First try exact match + for (EmlParser.EmailAttachment attachment : attachments) { + if (attachment.getFilename() != null) { + String normalizedAttachment = normalizeFilename(attachment.getFilename()); + if (normalizedAttachment.equals(normalizedTarget)) { + return attachment; + } + } + } + + // Then try contains match + for (EmlParser.EmailAttachment attachment : attachments) { + if (attachment.getFilename() != null) { + String normalizedAttachment = normalizeFilename(attachment.getFilename()); + if (normalizedAttachment.contains(normalizedTarget) + || normalizedTarget.contains(normalizedAttachment)) { + return attachment; + } + } + } + + return null; + } + + private static String findEmbeddedFilenameForAttachment( + EmlParser.EmailAttachment attachment, Map indexToFilenameMap) { + + String attachmentFilename = attachment.getFilename(); + if (attachmentFilename == null) { + return null; + } + + for (Map.Entry entry : indexToFilenameMap.entrySet()) { + String embeddedFilename = entry.getValue(); + if (embeddedFilename != null + && (embeddedFilename.equals(attachmentFilename) + || embeddedFilename.contains(attachmentFilename) + || attachmentFilename.contains(embeddedFilename))) { + return embeddedFilename; + } + } + + return null; + } + + private static String normalizeFilename(String filename) { + if (filename == null) return ""; + return filename.toLowerCase() + .trim() + .replaceAll("\\s+", " ") + .replaceAll("[^a-zA-Z0-9._-]", ""); + } + + private static void addAttachmentAnnotationToPageWithMapping( + PDDocument document, + PDPage page, + EmlParser.EmailAttachment attachment, + String embeddedFilename, + float x, + float y, + int attachmentIndex) + throws IOException { + + PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment(); + + PDRectangle rect = calculateAnnotationRectangle(page, x, y); + fileAnnotation.setRectangle(rect); + + fileAnnotation.setPrinted(false); + fileAnnotation.setHidden(false); + fileAnnotation.setNoView(false); + fileAnnotation.setNoZoom(true); + fileAnnotation.setNoRotate(true); + + try { + PDAppearanceDictionary appearance = new PDAppearanceDictionary(); + PDAppearanceStream normalAppearance = new PDAppearanceStream(document); + normalAppearance.setBBox(new PDRectangle(0, 0, rect.getWidth(), rect.getHeight())); + appearance.setNormalAppearance(normalAppearance); + fileAnnotation.setAppearance(appearance); + } catch (RuntimeException e) { + fileAnnotation.setAppearance(null); + } + + PDEmbeddedFilesNameTreeNode efTree = + document.getDocumentCatalog().getNames().getEmbeddedFiles(); + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + PDComplexFileSpecification fileSpec = efMap.get(embeddedFilename); + if (fileSpec != null) { + fileAnnotation.setFile(fileSpec); + } else { + // Could not find embedded file + } + } + } + + fileAnnotation.setContents( + "Attachment " + (attachmentIndex + 1) + ": " + attachment.getFilename()); + fileAnnotation.setAnnotationName( + "EmbeddedFile_" + attachmentIndex + "_" + embeddedFilename); + + page.getAnnotations().add(fileAnnotation); + } + + private static boolean isAscii(String str) { + if (str == null) return true; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) > 127) { + return false; + } + } + return true; + } +} From 774b500159e643cd06d8090eacd78832dc524388 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:19:19 +0100 Subject: [PATCH 55/71] get updates advanced (#4124) # Description of Changes This pull request introduces a comprehensive update to the application's update notification and modal system, enhancing both the backend logic and the user interface for update alerts. The changes include a new modal dialog for update details, improved internationalization (i18n) support, dynamic fetching of update information, and context-aware download links. These improvements make update notifications clearer, more informative, and tailored to the user's installation type. **Key changes:** **1. Update Notification and Modal System Overhaul** - Added a new modal dialog (`showUpdateModal`) that displays detailed update information, including current, latest, and latest stable versions, update priority, breaking changes, migration guides, and a list of available updates. The modal dynamically fetches and displays full update details and adapts to dark mode. ([[app/core/src/main/resources/static/js/githubVersion.jsR206-R387](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) - Enhanced the update button logic to reflect update priority visually (e.g., urgent/normal/minor), store summary data, and trigger the modal on click. ([[app/core/src/main/resources/static/js/githubVersion.jsL74-R190](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) - Improved the update check process to use a new summary API endpoint and handle missing or failed update data gracefully. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) **2. Context-Aware Download Links** - Introduced `getDownloadUrl()` to generate download links based on the user's machine type and security configuration, ensuring only relevant installers or jars are offered. ([[app/core/src/main/resources/static/js/githubVersion.jsL19-R108](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)]) **3. Internationalization (i18n) Enhancements** - Added new i18n keys for all update-related modal and notification strings in `messages_en_GB.properties`. ([[app/core/src/main/resources/messages_en_GB.propertiesR369-R400](diffhunk://#diff-ee1c6999a33498cfa3abba4a384e73a8b8269856899438de80560c965079a9fdR369-R400)]) - Injected all necessary i18n constants into the frontend via `navbar.html` for use in the modal and notifications. ([[app/core/src/main/resources/templates/fragments/navbar.htmlR14-R51](diffhunk://#diff-e7ef383033ea52a00c96e71d5d2c1ff08829078fa5c84c8e48e1bf8f48861ec6R14-R51)]) **4. General UI and Code Improvements** - Ensured update button styling is reset before applying new styles and improved accessibility by hiding the settings modal when the update modal is shown. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR138)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) These changes collectively provide a more robust, user-friendly, and maintainable update notification experience. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Reece Browne Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Co-authored-by: a --- .../main/resources/messages_en_GB.properties | 32 ++ .../main/resources/static/js/githubVersion.js | 314 +++++++++++++++++- .../resources/templates/fragments/navbar.html | 35 ++ build.gradle | 2 +- 4 files changed, 368 insertions(+), 15 deletions(-) diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 37be2c06a..f619b7b6e 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Settings settings.update=Update available settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Choose download option (For single file non zip downloads): settings.downloadOption.1=Open in same window diff --git a/app/core/src/main/resources/static/js/githubVersion.js b/app/core/src/main/resources/static/js/githubVersion.js index 2aef90d8c..ffc22ed08 100644 --- a/app/core/src/main/resources/static/js/githubVersion.js +++ b/app/core/src/main/resources/static/js/githubVersion.js @@ -16,21 +16,96 @@ function compareVersions(version1, version2) { return 0; } -async function getLatestReleaseVersion() { - const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest"; +function getDownloadUrl() { + // Only show download for non-Docker installations + if (machineType === 'Docker' || machineType === 'Kubernetes') { + return null; + } + + const baseUrl = 'https://files.stirlingpdf.com/'; + + // Determine file based on machine type and security + if (machineType === 'Server-jar') { + return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar'); + } + + // Client installations + if (machineType.startsWith('Client-')) { + const os = machineType.replace('Client-', ''); // win, mac, unix + const type = activeSecurity ? '-server-security' : '-server'; + + if (os === 'unix') { + return baseUrl + os + type + '.jar'; + } else if (os === 'win') { + return baseUrl + os + '-installer.exe'; + } else if (os === 'mac') { + return baseUrl + os + '-installer.dmg'; + } + } + + return null; +} + +// Function to get translated priority text +function getTranslatedPriority(priority) { + switch(priority?.toLowerCase()) { + case 'urgent': return updatePriorityUrgent; + case 'normal': return updatePriorityNormal; + case 'minor': return updatePriorityMinor; + case 'low': return updatePriorityLow; + default: return priority?.toUpperCase() || 'NORMAL'; + } +} + +async function getUpdateSummary() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`; + console.log("Fetching update summary from:", url); try { const response = await fetch(url); + console.log("Response status:", response.status); if (response.status === 200) { const data = await response.json(); - return data.tag_name ? data.tag_name.substring(1) : ""; + return data; } else { - // If the status is not 200, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", response.status); + return null; } } catch (error) { - console.error("Failed to fetch latest version from GitHub:", error); - // If an error occurs, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", error); + return null; + } +} + +async function getFullUpdateInfo() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`; + console.log("Fetching full update info from:", url); + try { + const response = await fetch(url); + console.log("Full update response status:", response.status); + if (response.status === 200) { + const data = await response.json(); + return data; + } else { + console.error("Failed to fetch full update info from Supabase:", response.status); + return null; + } + } catch (error) { + console.error("Failed to fetch full update info from Supabase:", error); + return null; } } @@ -60,6 +135,7 @@ async function checkForUpdate() { var updateLinkLegacy = document.getElementById("update-link-legacy") || null; if (updateBtn !== null) { updateBtn.style.display = "none"; + updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary"); } if (updateLink !== null) { updateLink.style.display = "none"; @@ -71,19 +147,47 @@ async function checkForUpdate() { } } - const latestVersion = await getLatestReleaseVersion(); - console.log("latestVersion=" + latestVersion); + const updateSummary = await getUpdateSummary(); + if (!updateSummary) { + console.log("No update summary available"); + return; + } + + console.log("updateSummary=", updateSummary); console.log("currentVersion=" + currentVersion); - console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion)); - if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) { + console.log("latestVersion=" + updateSummary.latest_version); + + if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) { + const priority = updateSummary.max_priority || 'normal'; + if (updateBtn != null) { - document.getElementById("update-btn").style.display = "block"; + // Style button based on priority + if (priority === 'urgent') { + updateBtn.classList.add("btn-danger"); + updateBtn.innerHTML = urgentUpdateAvailable; + } else if (priority === 'normal') { + updateBtn.classList.add("btn-warning"); + updateBtn.innerHTML = updateAvailableText; + } else { + updateBtn.classList.add("btn-outline-primary"); + updateBtn.innerHTML = updateAvailableText; + } + + // Store summary for initial display + updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary)); + updateBtn.style.display = "block"; + + // Add click handler for update details modal + updateBtn.onclick = function(e) { + e.preventDefault(); + showUpdateModal(); + }; } if (updateLink !== null) { document.getElementById("update-link").style.display = "flex"; } if (updateLinkLegacy !== null) { - document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + latestVersion + ''); + document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + updateSummary.latest_version + ''); if (updateLinkLegacy.classList.contains("visually-hidden")) { updateLinkLegacy.classList.remove("visually-hidden"); } @@ -99,6 +203,188 @@ async function checkForUpdate() { } } +async function showUpdateModal() { + // Close settings modal if open + const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal')); + if (settingsModal) { + settingsModal.hide(); + } + + // Get summary data from button + const updateBtn = document.getElementById("update-btn"); + const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary')); + + // Utility function to escape HTML special characters + function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + // Create initial modal with loading state + const initialModalHtml = ` + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('updateModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', initialModalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('updateModal')); + modal.show(); + + // Fetch full update info + const fullUpdateInfo = await getFullUpdateInfo(); + + // Update modal with full information + const modalBody = document.getElementById('updateModalBody'); + if (fullUpdateInfo && fullUpdateInfo.new_versions) { + const storedMode = localStorage.getItem("dark-mode"); + const isDarkMode = storedMode === "on" || + (storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); + const darkClasses = isDarkMode ? { + accordionItem: 'bg-dark border-secondary text-light', + accordionButton: 'bg-dark text-light border-secondary', + accordionBody: 'bg-dark text-light' + } : { + accordionItem: '', + accordionButton: '', + accordionBody: '' + }; + + const detailedVersionsHtml = ` +
+
${updateAvailableUpdates}
+
+ ${fullUpdateInfo.new_versions.map((version, index) => ` +
+

+ +

+
+
+
${version.announcement.title}
+

${version.announcement.message}

+ ${version.compatibility.breaking_changes ? ` + + ` : ''} +
+
+
+ `).join('')} +
+
+ `; + + // Remove loading spinner and add detailed info + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.remove(); + } + modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml); + + } else { + // Remove loading spinner if failed to load + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.innerHTML = `

${updateUnableToLoadDetails}

`; + } + } +} + document.addEventListener("DOMContentLoaded", (event) => { checkForUpdate(); }); diff --git a/app/core/src/main/resources/templates/fragments/navbar.html b/app/core/src/main/resources/templates/fragments/navbar.html index e5aea9345..833d4fd91 100644 --- a/app/core/src/main/resources/templates/fragments/navbar.html +++ b/app/core/src/main/resources/templates/fragments/navbar.html @@ -11,9 +11,44 @@ diff --git a/build.gradle b/build.gradle index ec786e2ed..39672cf24 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.1.1' + version = '1.1.2' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' From e6a77e83da2aa107b943097e50fe475615c6c665 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:36:58 +0100 Subject: [PATCH 56/71] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#4144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .github/labels.yml | 2 +- app/core/src/main/resources/static/js/githubVersion.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index b6cd969f6..a79fb8be5 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -182,4 +182,4 @@ description: "Pull request has been deployed to a test environment" - name: "codex" color: "ededed" - description: "chatgpt AI generated code" \ No newline at end of file + description: "chatgpt AI generated code" diff --git a/app/core/src/main/resources/static/js/githubVersion.js b/app/core/src/main/resources/static/js/githubVersion.js index ffc22ed08..5dee33238 100644 --- a/app/core/src/main/resources/static/js/githubVersion.js +++ b/app/core/src/main/resources/static/js/githubVersion.js @@ -321,7 +321,7 @@ async function showUpdateModal() { const modalBody = document.getElementById('updateModalBody'); if (fullUpdateInfo && fullUpdateInfo.new_versions) { const storedMode = localStorage.getItem("dark-mode"); - const isDarkMode = storedMode === "on" || + const isDarkMode = storedMode === "on" || (storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); const darkClasses = isDarkMode ? { accordionItem: 'bg-dark border-secondary text-light', From 6675a8af990dd541bab996091fc7bd716f719387 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:37:18 +0100 Subject: [PATCH 57/71] :globe_with_meridians: Sync Translations + Update README Progress Table (#4143) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request --------- Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 78 +++++++++---------- .../main/resources/messages_ar_AR.properties | 32 ++++++++ .../main/resources/messages_az_AZ.properties | 32 ++++++++ .../main/resources/messages_bg_BG.properties | 32 ++++++++ .../main/resources/messages_bo_CN.properties | 32 ++++++++ .../main/resources/messages_ca_CA.properties | 32 ++++++++ .../main/resources/messages_cs_CZ.properties | 32 ++++++++ .../main/resources/messages_da_DK.properties | 32 ++++++++ .../main/resources/messages_de_DE.properties | 32 ++++++++ .../main/resources/messages_el_GR.properties | 32 ++++++++ .../main/resources/messages_en_US.properties | 32 ++++++++ .../main/resources/messages_es_ES.properties | 32 ++++++++ .../main/resources/messages_eu_ES.properties | 32 ++++++++ .../main/resources/messages_fa_IR.properties | 32 ++++++++ .../main/resources/messages_fr_FR.properties | 32 ++++++++ .../main/resources/messages_ga_IE.properties | 32 ++++++++ .../main/resources/messages_hi_IN.properties | 32 ++++++++ .../main/resources/messages_hr_HR.properties | 32 ++++++++ .../main/resources/messages_hu_HU.properties | 32 ++++++++ .../main/resources/messages_id_ID.properties | 32 ++++++++ .../main/resources/messages_it_IT.properties | 32 ++++++++ .../main/resources/messages_ja_JP.properties | 32 ++++++++ .../main/resources/messages_ko_KR.properties | 32 ++++++++ .../main/resources/messages_ml_IN.properties | 32 ++++++++ .../main/resources/messages_nl_NL.properties | 32 ++++++++ .../main/resources/messages_no_NB.properties | 32 ++++++++ .../main/resources/messages_pl_PL.properties | 32 ++++++++ .../main/resources/messages_pt_BR.properties | 32 ++++++++ .../main/resources/messages_pt_PT.properties | 32 ++++++++ .../main/resources/messages_ro_RO.properties | 32 ++++++++ .../main/resources/messages_ru_RU.properties | 32 ++++++++ .../main/resources/messages_sk_SK.properties | 32 ++++++++ .../main/resources/messages_sl_SI.properties | 32 ++++++++ .../resources/messages_sr_LATN_RS.properties | 32 ++++++++ .../main/resources/messages_sv_SE.properties | 32 ++++++++ .../main/resources/messages_th_TH.properties | 32 ++++++++ .../main/resources/messages_tr_TR.properties | 32 ++++++++ .../main/resources/messages_uk_UA.properties | 32 ++++++++ .../main/resources/messages_vi_VN.properties | 32 ++++++++ .../main/resources/messages_zh_CN.properties | 32 ++++++++ .../main/resources/messages_zh_TW.properties | 32 ++++++++ 41 files changed, 1319 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b9660ce43..3b582cbfc 100644 --- a/README.md +++ b/README.md @@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages! | Language | Progress | | -------------------------------------------- | -------------------------------------- | -| Arabic (العربية) (ar_AR) | ![63%](https://geps.dev/progress/63) | -| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![63%](https://geps.dev/progress/63) | -| Basque (Euskara) (eu_ES) | ![37%](https://geps.dev/progress/37) | -| Bulgarian (Български) (bg_BG) | ![70%](https://geps.dev/progress/70) | -| Catalan (Català) (ca_CA) | ![69%](https://geps.dev/progress/69) | -| Croatian (Hrvatski) (hr_HR) | ![62%](https://geps.dev/progress/62) | -| Czech (Česky) (cs_CZ) | ![71%](https://geps.dev/progress/71) | -| Danish (Dansk) (da_DK) | ![63%](https://geps.dev/progress/63) | -| Dutch (Nederlands) (nl_NL) | ![61%](https://geps.dev/progress/61) | +| Arabic (العربية) (ar_AR) | ![61%](https://geps.dev/progress/61) | +| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![62%](https://geps.dev/progress/62) | +| Basque (Euskara) (eu_ES) | ![36%](https://geps.dev/progress/36) | +| Bulgarian (Български) (bg_BG) | ![68%](https://geps.dev/progress/68) | +| Catalan (Català) (ca_CA) | ![68%](https://geps.dev/progress/68) | +| Croatian (Hrvatski) (hr_HR) | ![60%](https://geps.dev/progress/60) | +| Czech (Česky) (cs_CZ) | ![70%](https://geps.dev/progress/70) | +| Danish (Dansk) (da_DK) | ![61%](https://geps.dev/progress/61) | +| Dutch (Nederlands) (nl_NL) | ![60%](https://geps.dev/progress/60) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | -| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) | -| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | -| Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) | -| Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) | -| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | -| Indonesian (Bahasa Indonesia) (id_ID) | ![63%](https://geps.dev/progress/63) | -| Irish (Gaeilge) (ga_IE) | ![70%](https://geps.dev/progress/70) | -| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | -| Japanese (日本語) (ja_JP) | ![95%](https://geps.dev/progress/95) | -| Korean (한국어) (ko_KR) | ![69%](https://geps.dev/progress/69) | -| Norwegian (Norsk) (no_NB) | ![67%](https://geps.dev/progress/67) | -| Persian (فارسی) (fa_IR) | ![66%](https://geps.dev/progress/66) | -| Polish (Polski) (pl_PL) | ![73%](https://geps.dev/progress/73) | -| Portuguese (Português) (pt_PT) | ![70%](https://geps.dev/progress/70) | -| Portuguese Brazilian (Português) (pt_BR) | ![77%](https://geps.dev/progress/77) | -| Romanian (Română) (ro_RO) | ![59%](https://geps.dev/progress/59) | -| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) | -| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![97%](https://geps.dev/progress/97) | -| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) | -| Slovakian (Slovensky) (sk_SK) | ![53%](https://geps.dev/progress/53) | -| Slovenian (Slovenščina) (sl_SI) | ![73%](https://geps.dev/progress/73) | -| Spanish (Español) (es_ES) | ![75%](https://geps.dev/progress/75) | -| Swedish (Svenska) (sv_SE) | ![67%](https://geps.dev/progress/67) | -| Thai (ไทย) (th_TH) | ![60%](https://geps.dev/progress/60) | -| Tibetan (བོད་ཡིག་) (bo_CN) | ![66%](https://geps.dev/progress/66) | -| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) | -| Turkish (Türkçe) (tr_TR) | ![82%](https://geps.dev/progress/82) | -| Ukrainian (Українська) (uk_UA) | ![72%](https://geps.dev/progress/72) | -| Vietnamese (Tiếng Việt) (vi_VN) | ![58%](https://geps.dev/progress/58) | -| Malayalam (മലയാളം) (ml_IN) | ![75%](https://geps.dev/progress/75) | +| French (Français) (fr_FR) | ![89%](https://geps.dev/progress/89) | +| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) | +| Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) | +| Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) | +| Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | +| Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | +| Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | +| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) | +| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | +| Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) | +| Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) | +| Persian (فارسی) (fa_IR) | ![64%](https://geps.dev/progress/64) | +| Polish (Polski) (pl_PL) | ![72%](https://geps.dev/progress/72) | +| Portuguese (Português) (pt_PT) | ![69%](https://geps.dev/progress/69) | +| Portuguese Brazilian (Português) (pt_BR) | ![76%](https://geps.dev/progress/76) | +| Romanian (Română) (ro_RO) | ![57%](https://geps.dev/progress/57) | +| Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) | +| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![95%](https://geps.dev/progress/95) | +| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) | +| Slovakian (Slovensky) (sk_SK) | ![51%](https://geps.dev/progress/51) | +| Slovenian (Slovenščina) (sl_SI) | ![71%](https://geps.dev/progress/71) | +| Spanish (Español) (es_ES) | ![74%](https://geps.dev/progress/74) | +| Swedish (Svenska) (sv_SE) | ![65%](https://geps.dev/progress/65) | +| Thai (ไทย) (th_TH) | ![59%](https://geps.dev/progress/59) | +| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) | +| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) | +| Turkish (Türkçe) (tr_TR) | ![80%](https://geps.dev/progress/80) | +| Ukrainian (Українська) (uk_UA) | ![71%](https://geps.dev/progress/71) | +| Vietnamese (Tiếng Việt) (vi_VN) | ![57%](https://geps.dev/progress/57) | +| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) | ## Stirling PDF Enterprise diff --git a/app/core/src/main/resources/messages_ar_AR.properties b/app/core/src/main/resources/messages_ar_AR.properties index 71bedd8e2..1cd554cd1 100644 --- a/app/core/src/main/resources/messages_ar_AR.properties +++ b/app/core/src/main/resources/messages_ar_AR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=المفضل settings.title=الإعدادات settings.update=التحديث متاح settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=إصدار التطبيق: settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط): settings.downloadOption.1=فتح في نفس النافذة diff --git a/app/core/src/main/resources/messages_az_AZ.properties b/app/core/src/main/resources/messages_az_AZ.properties index 151dc0e64..2304a13d1 100644 --- a/app/core/src/main/resources/messages_az_AZ.properties +++ b/app/core/src/main/resources/messages_az_AZ.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populyar settings.title=Parametrlər settings.update=Yeniləmə mövcuddur settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Proqram Versiyası: settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün): settings.downloadOption.1=Eyni pəncərədə açın diff --git a/app/core/src/main/resources/messages_bg_BG.properties b/app/core/src/main/resources/messages_bg_BG.properties index 63b0c0b85..a99e9447e 100644 --- a/app/core/src/main/resources/messages_bg_BG.properties +++ b/app/core/src/main/resources/messages_bg_BG.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярни settings.title=Настройки settings.update=Налична актуализация settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версия на приложението: settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран): settings.downloadOption.1=Отваряне в същия прозорец diff --git a/app/core/src/main/resources/messages_bo_CN.properties b/app/core/src/main/resources/messages_bo_CN.properties index 5b39cdcf5..aef66f128 100644 --- a/app/core/src/main/resources/messages_bo_CN.properties +++ b/app/core/src/main/resources/messages_bo_CN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=སྤྱི་མོས། settings.title=སྒྲིག་འགོད། settings.update=གསར་སྒྱུར་ཡོད། settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད། + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=མཉེན་ཆས་པར་གཞི། settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།): settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ། diff --git a/app/core/src/main/resources/messages_ca_CA.properties b/app/core/src/main/resources/messages_ca_CA.properties index a8f9a560f..ff7f2b64b 100644 --- a/app/core/src/main/resources/messages_ca_CA.properties +++ b/app/core/src/main/resources/messages_ca_CA.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Opcions settings.update=Actualització Disponible settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versió de l'App: settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit): settings.downloadOption.1=Obre en la mateixa finestra diff --git a/app/core/src/main/resources/messages_cs_CZ.properties b/app/core/src/main/resources/messages_cs_CZ.properties index a83268aa2..a68fbcb78 100644 --- a/app/core/src/main/resources/messages_cs_CZ.properties +++ b/app/core/src/main/resources/messages_cs_CZ.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Oblíbené settings.title=Nastavení settings.update=K dispozici je aktualizace settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verze aplikace: settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu): settings.downloadOption.1=Otevřít ve stejném okně diff --git a/app/core/src/main/resources/messages_da_DK.properties b/app/core/src/main/resources/messages_da_DK.properties index bc06c0915..8d55cc8d1 100644 --- a/app/core/src/main/resources/messages_da_DK.properties +++ b/app/core/src/main/resources/messages_da_DK.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populære settings.title=Indstillinger settings.update=Opdatering tilgængelig settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads): settings.downloadOption.1=Åbn i samme vindue diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index 1bb923450..63b54fa74 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Beliebt settings.title=Einstellungen settings.update=Update verfügbar settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App-Version: settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind): settings.downloadOption.1=Im selben Fenster öffnen diff --git a/app/core/src/main/resources/messages_el_GR.properties b/app/core/src/main/resources/messages_el_GR.properties index e4209faf8..a9fbee538 100644 --- a/app/core/src/main/resources/messages_el_GR.properties +++ b/app/core/src/main/resources/messages_el_GR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Δημοφιλή settings.title=Ρυθμίσεις settings.update=Διαθέσιμη ενημέρωση settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Έκδοση εφαρμογής: settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση): settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index e6bad97d0..877c25e75 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Settings settings.update=Update available settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Choose download option (For single file non zip downloads): settings.downloadOption.1=Open in same window diff --git a/app/core/src/main/resources/messages_es_ES.properties b/app/core/src/main/resources/messages_es_ES.properties index 40fe58987..4ccb6d758 100644 --- a/app/core/src/main/resources/messages_es_ES.properties +++ b/app/core/src/main/resources/messages_es_ES.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populares settings.title=Configuración settings.update=Actualización disponible settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versión de la aplicación: settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP): settings.downloadOption.1=Abrir en la misma ventana diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 92bb97c63..513f5241e 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Ezarpenak settings.update=Eguneratze eskuragarria settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Aplikazioaren bertsioa: settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe): settings.downloadOption.1=Ireki leiho berean diff --git a/app/core/src/main/resources/messages_fa_IR.properties b/app/core/src/main/resources/messages_fa_IR.properties index 02e44b563..dccb7fc0b 100644 --- a/app/core/src/main/resources/messages_fa_IR.properties +++ b/app/core/src/main/resources/messages_fa_IR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=محبوب settings.title=تنظیمات settings.update=به‌روزرسانی موجود است settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=نسخه برنامه: settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده): settings.downloadOption.1=باز کردن در همان پنجره diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index f45f94078..7f53edbfe 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populaire settings.title=Paramètres settings.update=Mise à jour disponible settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Version de l'application : settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) : settings.downloadOption.1=Ouvrir dans la même fenêtre diff --git a/app/core/src/main/resources/messages_ga_IE.properties b/app/core/src/main/resources/messages_ga_IE.properties index 874c8ebca..816932ff1 100644 --- a/app/core/src/main/resources/messages_ga_IE.properties +++ b/app/core/src/main/resources/messages_ga_IE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Coitianta settings.title=Socruithe settings.update=Nuashonrú ar fáil settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Leagan Aipe: settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip): settings.downloadOption.1=Oscail sa bhfuinneog chéanna diff --git a/app/core/src/main/resources/messages_hi_IN.properties b/app/core/src/main/resources/messages_hi_IN.properties index 369d9444c..e2f9b2c19 100644 --- a/app/core/src/main/resources/messages_hi_IN.properties +++ b/app/core/src/main/resources/messages_hi_IN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=लोकप्रिय settings.title=सेटिंग्स settings.update=अपडेट उपलब्ध है settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है। + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=ऐप संस्करण: settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए): settings.downloadOption.1=उसी विंडो में खोलें diff --git a/app/core/src/main/resources/messages_hr_HR.properties b/app/core/src/main/resources/messages_hr_HR.properties index 87a4add1d..7ea02b909 100644 --- a/app/core/src/main/resources/messages_hr_HR.properties +++ b/app/core/src/main/resources/messages_hr_HR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularno settings.title=Postavke settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzija aplikacije: settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata): settings.downloadOption.1=Otvori u istom prozoru diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 490dbecce..45de2334c 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Népszerű settings.title=Beállítások settings.update=Frissítés elérhető settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Alkalmazás verziója: settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén): settings.downloadOption.1=Megnyitás ugyanabban az ablakban diff --git a/app/core/src/main/resources/messages_id_ID.properties b/app/core/src/main/resources/messages_id_ID.properties index 470945372..541226f69 100644 --- a/app/core/src/main/resources/messages_id_ID.properties +++ b/app/core/src/main/resources/messages_id_ID.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populer settings.title=Pengaturan settings.update=Pembaruan tersedia settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versi Aplikasi: settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip): settings.downloadOption.1=Buka di jendela yang sama diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 71c0f9ffc..0db465a40 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popolare settings.title=Impostazioni settings.update=Aggiornamento disponibile settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versione App: settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi): settings.downloadOption.1=Apri in questa finestra diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index fdffa3523..ced0c7c56 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -366,6 +366,38 @@ navbar.sections.popular=人気 settings.title=設定 settings.update=利用可能なアップデート settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Appバージョン: settings.downloadOption.title=ダウンロードオプション(zip以外の単一ファイル): settings.downloadOption.1=同じウィンドウで開く diff --git a/app/core/src/main/resources/messages_ko_KR.properties b/app/core/src/main/resources/messages_ko_KR.properties index b129e9c69..7de79d52c 100644 --- a/app/core/src/main/resources/messages_ko_KR.properties +++ b/app/core/src/main/resources/messages_ko_KR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=인기 settings.title=설정 settings.update=업데이트 가능 settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=앱 버전: settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용): settings.downloadOption.1=같은 창에서 열기 diff --git a/app/core/src/main/resources/messages_ml_IN.properties b/app/core/src/main/resources/messages_ml_IN.properties index 775b68792..123f5a53f 100644 --- a/app/core/src/main/resources/messages_ml_IN.properties +++ b/app/core/src/main/resources/messages_ml_IN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=ജനപ്രിയം settings.title=ക്രമീകരണങ്ങൾ settings.update=അപ്ഡേറ്റ് ലഭ്യമാണ് settings.updateAvailable={0} നിലവിൽ ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പാണ്. ഒരു പുതിയ പതിപ്പ് ({1}) ലഭ്യമാണ്. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=ആപ്പ് പതിപ്പ്: settings.downloadOption.title=ഡൗൺലോഡ് ഓപ്ഷൻ തിരഞ്ഞെടുക്കുക (സിംഗിൾ ഫയൽ നോൺ-സിപ്പ് ഡൗൺലോഡുകൾക്ക്): settings.downloadOption.1=ഒരേ വിൻഡോയിൽ തുറക്കുക diff --git a/app/core/src/main/resources/messages_nl_NL.properties b/app/core/src/main/resources/messages_nl_NL.properties index 94b1bb020..44418eb0f 100644 --- a/app/core/src/main/resources/messages_nl_NL.properties +++ b/app/core/src/main/resources/messages_nl_NL.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Instellingen settings.update=Update beschikbaar settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App versie: settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip): settings.downloadOption.1=Open in hetzelfde venster diff --git a/app/core/src/main/resources/messages_no_NB.properties b/app/core/src/main/resources/messages_no_NB.properties index dadc0bc32..ed830ec3f 100644 --- a/app/core/src/main/resources/messages_no_NB.properties +++ b/app/core/src/main/resources/messages_no_NB.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populært settings.title=Innstillinger settings.update=Oppdatering tilgjengelig settings.updateAvailable={0} er den nåværende installerte versjonen. En ny versjon ({1}) er tilgjengelig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Versjon: settings.downloadOption.title=Velg nedlastingsalternativ (For enkeltfil ikke-zip nedlastinger): settings.downloadOption.1=Åpne i samme vindu diff --git a/app/core/src/main/resources/messages_pl_PL.properties b/app/core/src/main/resources/messages_pl_PL.properties index 7d553c574..0eefb4ccc 100644 --- a/app/core/src/main/resources/messages_pl_PL.properties +++ b/app/core/src/main/resources/messages_pl_PL.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularne settings.title=Ustawienia settings.update=Dostępna aktualizacja settings.updateAvailable=Wersja {0} jest obecenia zainstalowana, dostępna jest nowa wersja ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Wersja aplikacji: settings.downloadOption.title=Wybierz opcję pobierania (w przypadku pobierania pojedynczych plików innych niż ZIP): settings.downloadOption.1=Otwórz w tym samym oknie diff --git a/app/core/src/main/resources/messages_pt_BR.properties b/app/core/src/main/resources/messages_pt_BR.properties index cde839e5e..57e8dd93e 100644 --- a/app/core/src/main/resources/messages_pt_BR.properties +++ b/app/core/src/main/resources/messages_pt_BR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populares settings.title=Configurações settings.update=Atualização disponível settings.updateAvailable={0} é a versão atualmente instalada. Uma nova versão ({1}) está disponível. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versão do Aplicativo: settings.downloadOption.title=Escolha a opção de download (para download de arquivo único, não compactados): settings.downloadOption.1=Abrir na mesma janela diff --git a/app/core/src/main/resources/messages_pt_PT.properties b/app/core/src/main/resources/messages_pt_PT.properties index 49998f273..2c78fa93b 100644 --- a/app/core/src/main/resources/messages_pt_PT.properties +++ b/app/core/src/main/resources/messages_pt_PT.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Definições settings.update=Atualização disponível settings.updateAvailable={0} é a versão atual instalada. Uma nova versão ({1}) está disponível. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versão da Aplicação: settings.downloadOption.title=Escolha a opção de download (Para downloads de ficheiro único não zipado): settings.downloadOption.1=Abrir na mesma janela diff --git a/app/core/src/main/resources/messages_ro_RO.properties b/app/core/src/main/resources/messages_ro_RO.properties index e33d01f4a..5a904a9c8 100644 --- a/app/core/src/main/resources/messages_ro_RO.properties +++ b/app/core/src/main/resources/messages_ro_RO.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Setări settings.update=Actualizare disponibilă settings.updateAvailable={0} este versiunea instalată curent. O nouă versiune ({1}) este disponibilă. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versiune aplicație: settings.downloadOption.title=Alege opțiunea de descărcare (pentru descărcarea unui singur fișier non-zip): settings.downloadOption.1=Deschide în aceeași fereastră diff --git a/app/core/src/main/resources/messages_ru_RU.properties b/app/core/src/main/resources/messages_ru_RU.properties index 072e03123..4580f3933 100644 --- a/app/core/src/main/resources/messages_ru_RU.properties +++ b/app/core/src/main/resources/messages_ru_RU.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярное settings.title=Настройки settings.update=Доступно обновление settings.updateAvailable=Текущая установленная версия - {0}. Доступна новая версия ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версия приложения: settings.downloadOption.title=Выберите вариант загрузки (для одиночных файлов без архивации): settings.downloadOption.1=Открыть в том же окне diff --git a/app/core/src/main/resources/messages_sk_SK.properties b/app/core/src/main/resources/messages_sk_SK.properties index 10ed3d985..68faeab85 100644 --- a/app/core/src/main/resources/messages_sk_SK.properties +++ b/app/core/src/main/resources/messages_sk_SK.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Nastavenia settings.update=Dostupná aktualizácia settings.updateAvailable={0} je aktuálne nainštalovaná verzia. Nová verzia ({1}) je dostupná. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzia aplikácie: settings.downloadOption.title=Vyberte možnosť sťahovania (Pre jednotlivé neskomprimované súbory): settings.downloadOption.1=Otvoriť v rovnakom okne diff --git a/app/core/src/main/resources/messages_sl_SI.properties b/app/core/src/main/resources/messages_sl_SI.properties index 8b15dcc42..fe95a4165 100644 --- a/app/core/src/main/resources/messages_sl_SI.properties +++ b/app/core/src/main/resources/messages_sl_SI.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Priljubljeno settings.title=Nastavitve settings.update=Na voljo je posodobitev settings.updateAvailable={0} je trenutno nameščena različica. Na voljo je nova različica ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Različica aplikacije: settings.downloadOption.title=Izberi možnost prenosa (za prenose ene datoteke brez zip): settings.downloadOption.1=Odpri v istem oknu diff --git a/app/core/src/main/resources/messages_sr_LATN_RS.properties b/app/core/src/main/resources/messages_sr_LATN_RS.properties index 305b68aa1..f15d8397a 100644 --- a/app/core/src/main/resources/messages_sr_LATN_RS.properties +++ b/app/core/src/main/resources/messages_sr_LATN_RS.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularno settings.title=Podešavanja settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Nova verzija ({1}) je dostupna. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzija aplikacije: settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih fajlova bez zip formata): settings.downloadOption.1=Otvori u istom prozoru diff --git a/app/core/src/main/resources/messages_sv_SE.properties b/app/core/src/main/resources/messages_sv_SE.properties index e731f6337..7a786add6 100644 --- a/app/core/src/main/resources/messages_sv_SE.properties +++ b/app/core/src/main/resources/messages_sv_SE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populära settings.title=Inställningar settings.update=Uppdatering tillgänglig settings.updateAvailable={0} är den aktuella installerade versionen. En ny version ({1}) finns tillgänglig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Appversion: settings.downloadOption.title=Välj nedladdningsalternativ (för nedladdning av en fil utan zip): settings.downloadOption.1=Öppnas i samma fönster diff --git a/app/core/src/main/resources/messages_th_TH.properties b/app/core/src/main/resources/messages_th_TH.properties index 7a2b20aea..9b332982c 100644 --- a/app/core/src/main/resources/messages_th_TH.properties +++ b/app/core/src/main/resources/messages_th_TH.properties @@ -366,6 +366,38 @@ navbar.sections.popular=ยอดนิยม settings.title=การตั้งค่า settings.update=มีการอัปเดต settings.updateAvailable={0} คือเวอร์ชันที่ติดตั้งในปัจจุบัน มีเวอร์ชันใหม่ ({1}) พร้อมให้บริการ + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=เวอร์ชันแอป: settings.downloadOption.title=เลือกตัวเลือกการดาวน์โหลด (สำหรับการดาวน์โหลดไฟล์เดียวที่ไม่ใช่ zip): settings.downloadOption.1=เปิดในหน้าต่างเดียวกัน diff --git a/app/core/src/main/resources/messages_tr_TR.properties b/app/core/src/main/resources/messages_tr_TR.properties index c03d7872e..72e78f1b3 100644 --- a/app/core/src/main/resources/messages_tr_TR.properties +++ b/app/core/src/main/resources/messages_tr_TR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Ayarlar settings.update=Güncelleme mevcut settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Uygulama Sürümü: settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için): settings.downloadOption.1=Aynı pencerede aç diff --git a/app/core/src/main/resources/messages_uk_UA.properties b/app/core/src/main/resources/messages_uk_UA.properties index f24d997ac..db5739fe3 100644 --- a/app/core/src/main/resources/messages_uk_UA.properties +++ b/app/core/src/main/resources/messages_uk_UA.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярне settings.title=Налаштування settings.update=Доступне оновлення settings.updateAvailable=Зараз встановлена версія {0}. Нова версія ({1}) доступна. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версія додатку: settings.downloadOption.title=Виберіть варіант завантаження (для завантаження одного файлу без zip): settings.downloadOption.1=Відкрити в тому ж вікні diff --git a/app/core/src/main/resources/messages_vi_VN.properties b/app/core/src/main/resources/messages_vi_VN.properties index cd2e412f7..0a1e9b392 100644 --- a/app/core/src/main/resources/messages_vi_VN.properties +++ b/app/core/src/main/resources/messages_vi_VN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Cài đặt settings.update=Có bản cập nhật settings.updateAvailable={0} là phiên bản hiện tại đã cài đặt. Một phiên bản mới ({1}) đã có sẵn. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Phiên bản ứng dụng: settings.downloadOption.title=Chọn tùy chọn tải xuống (Đối với tải xuống tệp đơn không nén): settings.downloadOption.1=Mở trong cùng cửa sổ diff --git a/app/core/src/main/resources/messages_zh_CN.properties b/app/core/src/main/resources/messages_zh_CN.properties index 252eb2768..4eeac6483 100644 --- a/app/core/src/main/resources/messages_zh_CN.properties +++ b/app/core/src/main/resources/messages_zh_CN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=热门 settings.title=设置 settings.update=有可用的更新 settings.updateAvailable=当前版本为 {0},新版本 ({1}) 可用。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=应用程序版本: settings.downloadOption.title=选择下载选项(单个文件非压缩文件): settings.downloadOption.1=在同一窗口打开 diff --git a/app/core/src/main/resources/messages_zh_TW.properties b/app/core/src/main/resources/messages_zh_TW.properties index 9f38178ac..cee6b9c7d 100644 --- a/app/core/src/main/resources/messages_zh_TW.properties +++ b/app/core/src/main/resources/messages_zh_TW.properties @@ -366,6 +366,38 @@ navbar.sections.popular=熱門功能 settings.title=設定 settings.update=有更新可用 settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=應用程式版本: settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載): settings.downloadOption.1=在同一視窗中開啟 From 71ac4283b27f727cb2d61e5e999ff7daadf6f5fa Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:39:47 +0100 Subject: [PATCH 58/71] PSD (#4146) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../common/util/ImageProcessingUtils.java | 32 ++++++++++++++++++- app/core/build.gradle | 2 +- .../main/resources/messages_en_GB.properties | 8 ++--- .../main/resources/messages_en_US.properties | 8 ++--- .../main/resources/messages_eu_ES.properties | 4 +-- .../main/resources/messages_fr_FR.properties | 2 +- .../main/resources/messages_ja_JP.properties | 4 +-- .../templates/convert/img-to-pdf.html | 2 +- build.gradle | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index ae6c0b66f..fd4091d4c 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -5,8 +5,11 @@ import java.awt.image.*; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Iterator; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import org.springframework.web.multipart.MultipartFile; @@ -115,7 +118,34 @@ public class ImageProcessingUtils { public static BufferedImage loadImageWithExifOrientation(MultipartFile file) throws IOException { - BufferedImage image = ImageIO.read(file.getInputStream()); + BufferedImage image = null; + String filename = file.getOriginalFilename(); + + if (filename != null && filename.toLowerCase().endsWith(".psd")) { + // For PSD files, try explicit ImageReader + Iterator readers = ImageIO.getImageReadersByFormatName("PSD"); + if (readers.hasNext()) { + ImageReader reader = readers.next(); + try (ImageInputStream iis = ImageIO.createImageInputStream(file.getInputStream())) { + reader.setInput(iis); + image = reader.read(0); + } finally { + reader.dispose(); + } + } + if (image == null) { + throw new IOException("Unable to read image from file: " + filename + + ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); + } + } else { + // For non-PSD files, use standard ImageIO + image = ImageIO.read(file.getInputStream()); + } + + if (image == null) { + throw new IOException("Unable to read image from file: " + filename); + } + double orientation = extractImageOrientation(file.getInputStream()); return applyOrientation(image, orientation); } diff --git a/app/core/build.gradle b/app/core/build.gradle index 037a89497..c9905a308 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -91,7 +91,7 @@ dependencies { // 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:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion" diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index f619b7b6e..d6056e856 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -601,12 +601,12 @@ rotate.tags=server side home.imageToPdf.title=Image to PDF -home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF. -imageToPdf.tags=conversion,img,jpg,picture,photo +home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDF to Image -home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) -pdfToImage.tags=conversion,img,jpg,picture,photo +home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Organise home.pdfOrganiser.desc=Remove/Rearrange pages in any order diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index 877c25e75..250dd51c5 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -601,12 +601,12 @@ rotate.tags=server side home.imageToPdf.title=Image to PDF -home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF. -imageToPdf.tags=conversion,img,jpg,picture,photo +home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDF to Image -home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) -pdfToImage.tags=conversion,img,jpg,picture,photo +home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Organize home.pdfOrganiser.desc=Remove/Rearrange pages in any order diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 513f5241e..27dbfdb08 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -602,11 +602,11 @@ rotate.tags=server side home.imageToPdf.title=Irudia PDF bihurtu home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu -imageToPdf.tags=conversion,img,jpg,picture,photo +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDFa irudi bihurtu home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu -pdfToImage.tags=conversion,img,jpg,picture,photo +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Antolatzailea home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index 7f53edbfe..86e6c0d95 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -601,7 +601,7 @@ rotate.tags=pivoter,server side,rotate home.imageToPdf.title=Image en PDF -home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF) en PDF. +home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF. imageToPdf.tags=pdf,conversion,img,jpg,image,photo home.pdfToImage.title=PDF en image diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index ced0c7c56..a5af895fd 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -602,11 +602,11 @@ rotate.tags=server side home.imageToPdf.title=画像をPDFに変換 home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。 -imageToPdf.tags=conversion,img,jpg,picture,photo +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDFを画像に変換 home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。 -pdfToImage.tags=conversion,img,jpg,picture,photo +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=整理 home.pdfOrganiser.desc=ページの削除/並べ替えします。 diff --git a/app/core/src/main/resources/templates/convert/img-to-pdf.html b/app/core/src/main/resources/templates/convert/img-to-pdf.html index 6c37e6473..c3b01eec2 100644 --- a/app/core/src/main/resources/templates/convert/img-to-pdf.html +++ b/app/core/src/main/resources/templates/convert/img-to-pdf.html @@ -22,7 +22,7 @@
+ th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*,.psd', inputText=#{imgPrompt})}">
diff --git a/build.gradle b/build.gradle index 39672cf24..e54c58e7d 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.1.2' + version = '1.2.0' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' From 678a9bc4636a2589e7fc32a7d23d08c852b391c0 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:53:45 +0100 Subject: [PATCH 59/71] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#4150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../software/common/util/ImageProcessingUtils.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index fd4091d4c..7140b3cc2 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -120,7 +120,7 @@ public class ImageProcessingUtils { throws IOException { BufferedImage image = null; String filename = file.getOriginalFilename(); - + if (filename != null && filename.toLowerCase().endsWith(".psd")) { // For PSD files, try explicit ImageReader Iterator readers = ImageIO.getImageReadersByFormatName("PSD"); @@ -134,18 +134,20 @@ public class ImageProcessingUtils { } } if (image == null) { - throw new IOException("Unable to read image from file: " + filename + - ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); + throw new IOException( + "Unable to read image from file: " + + filename + + ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); } } else { // For non-PSD files, use standard ImageIO image = ImageIO.read(file.getInputStream()); } - + if (image == null) { throw new IOException("Unable to read image from file: " + filename); } - + double orientation = extractImageOrientation(file.getInputStream()); return applyOrientation(image, orientation); } From 796873134f1c3c37fbfcdf7d6375fc21481be7aa Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:54:00 +0100 Subject: [PATCH 60/71] Update 3rd Party Licenses (#4122) Auto-generated by stirlingbot[bot] Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../main/resources/static/3rdPartyLicenses.json | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/core/src/main/resources/static/3rdPartyLicenses.json b/app/core/src/main/resources/static/3rdPartyLicenses.json index 59acd0fc2..23278a23f 100644 --- a/app/core/src/main/resources/static/3rdPartyLicenses.json +++ b/app/core/src/main/resources/static/3rdPartyLicenses.json @@ -336,6 +336,12 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-psd", + "moduleVersion": "3.12.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-tiff", "moduleVersion": "3.12.0", @@ -623,21 +629,21 @@ { "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -980,13 +986,13 @@ }, { "moduleName": "org.commonmark:commonmark", - "moduleVersion": "0.25.0", + "moduleVersion": "0.25.1", "moduleLicense": "BSD-2-Clause", "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" }, { "moduleName": "org.commonmark:commonmark-ext-gfm-tables", - "moduleVersion": "0.25.0", + "moduleVersion": "0.25.1", "moduleLicense": "BSD-2-Clause", "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" }, From e8b5ae0474a8333a2eb141aa18e185d73f608317 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Sat, 9 Aug 2025 00:07:20 +0200 Subject: [PATCH 61/71] Update messages_it_IT.properties (#4154) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../main/resources/messages_it_IT.properties | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 0db465a40..74952b670 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -368,36 +368,36 @@ settings.update=Aggiornamento disponibile settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Aggiornamento disponibile +update.updateAvailable=Aggiornamento disponibile +update.modalTitle=Aggiornamento disponibile +update.current=Corrente +update.latest=Ultimo +update.latestStable=Ultima versione stabile +update.priority=Priorità +update.recommendedAction=Azione consigliata +update.breakingChangesDetected=⚠️ Rilevate modifiche sostanziali +update.breakingChangesMessage=Questo aggiornamento contiene modifiche sostanziali. Consulta le guide alla migrazione riportate di seguito. +update.migrationGuides=Guide alla migrazione: +update.viewGuide=Visualizza la guida +update.loadingDetailedInfo=Caricamento delle informazioni dettagliate sulla versione... +update.close=Chiudi +update.viewAllReleases=Visualizza tutte le versioni +update.downloadLatest=Scarica l'ultima +update.availableUpdates=Aggiornamenti disponibili: +update.unableToLoadDetails=Impossibile caricare informazioni dettagliate sulla versione. +update.version=Versione # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=URGENTE +update.priority.normal=NORMALE +update.priority.minor=MINORE +update.priority.low=BASSA # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Modifiche sostanziali: +update.breakingChangesDefault=Questa versione contiene modifiche sostanziali +update.migrationGuide=Guida alla migrazione settings.appVersion=Versione App: settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi): settings.downloadOption.1=Apri in questa finestra @@ -1696,7 +1696,7 @@ fileChooser.dragAndDrop=Trascina & Rilascia fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF fileChooser.dragAndDropImage=Trascina & rilascia il file immagine fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui -fileChooser.extractPDF=Estraendo... +fileChooser.extractPDF=Estrazione... fileChooser.addAttachments=trascina & rilascia gli allegati qui #release notes @@ -1892,12 +1892,12 @@ editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deselezi editTableOfContents.editorTitle=Editor segnalibri editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari. editTableOfContents.addBookmark=Aggiungi nuovo segnalibro -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Importa +editTableOfContents.importBookmarksFromJsonFile=Carica file JSON +editTableOfContents.importBookmarksFromClipboard=Incolla dagli appunti +editTableOfContents.exportBookmarksDefault=Esporta +editTableOfContents.exportBookmarksAsJson=Scarica come JSON +editTableOfContents.exportBookmarksAsText=Copia come testo editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF. editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali. editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione. From 3938a07c132080cfb362145db0378803be3a58f8 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:13:33 +0100 Subject: [PATCH 62/71] :globe_with_meridians: Sync Translations + Update README Progress Table (#4155) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b582cbfc..5e212780a 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Stirling-PDF currently supports 40 languages! | Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | | Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | | Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | -| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) | +| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | | Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | | Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) | | Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) | From 5e01b15d3ca466abcae5a707aa6dc4c3de6f4e89 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:03:24 +0100 Subject: [PATCH 63/71] Update .files.yaml for V2 (#4156) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/config/.files.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 225470ea9..a5d8410f3 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -27,3 +27,5 @@ project: &project - gradlew.bat - launch4jConfig.xml - settings.gradle + - frontend/** + - docker/** From 299ce03dda733daf47a9da527dbc004fca3c5d34 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:09:26 +0100 Subject: [PATCH 64/71] Update CODEOWNERS (#4158) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d5389fda..f89c7154d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,20 +2,20 @@ * @Frooodle @Ludy87 @jbrunton96 @ConnorYoh # Backend -/app/** @DarioGii +/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh #V1 frontend -/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 -/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 +/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 #V2 frontend -/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle #V2 docker -/docker/backend/** @Frooodle @Ludy87 @DarioGii -/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 -/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 +/docker/backend/** @Frooodle @Ludy87 @DarioGii @Ludy87 +/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 +/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 #GHA (All users) -/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 +/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 From 05b5771c89ccb9cb1b357d14fba21e2013420b12 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sat, 9 Aug 2025 16:09:50 +0200 Subject: [PATCH 65/71] fix(saml): correct ClassPathResource handling for IdP metadata and add null-guard for privateKey (#4157) ## Description of Changes **What was changed** - In `getIdpMetadataUri()`, use `idpMetadataUri.substring("classpath:".length())` so the `classpath:` scheme (including the colon) is stripped correctly before creating the `ClassPathResource`. - In `getPrivateKey()`, add a null check (`if (privateKey == null) return null;`) to avoid a potential `NullPointerException` when the property is unset. **Why the change was made** - The previous substring used `"classpath".length()` (without the colon), leaving a leading `:` in the path (e.g., `:/saml/idp.xml`) which breaks `ClassPathResource` resolution and can prevent SAML bootstrapping when `idpMetadataUri` uses the `classpath:` scheme. - The null-guard aligns the method with defensive coding practices and prevents runtime errors when no private key is configured. --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] I have performed a self-review of my own code - [x] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../common/model/ApplicationProperties.java | 3 +- ...opertiesDynamicYamlPropertySourceTest.java | 59 +++++ .../model/ApplicationPropertiesLogicTest.java | 248 ++++++++++++++++++ .../ApplicationPropertiesSaml2HttpTest.java | 80 ++++++ ...pplicationPropertiesSaml2ResourceTest.java | 55 ++++ app/common/src/test/resources/saml/dummy.txt | 1 + build.gradle | 19 +- 7 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java create mode 100644 app/common/src/test/resources/saml/dummy.txt diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index fb93ef345..ee893c575 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -197,7 +197,7 @@ public class ApplicationProperties { @JsonIgnore public InputStream getIdpMetadataUri() throws IOException { if (idpMetadataUri.startsWith("classpath:")) { - return new ClassPathResource(idpMetadataUri.substring("classpath".length())) + return new ClassPathResource(idpMetadataUri.substring("classpath:".length())) .getInputStream(); } try { @@ -233,6 +233,7 @@ public class ApplicationProperties { @JsonIgnore public Resource getPrivateKey() { + if (privateKey == null) return null; if (privateKey.startsWith("classpath:")) { return new ClassPathResource(privateKey.substring("classpath:".length())); } else { diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java new file mode 100644 index 000000000..71d3997be --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java @@ -0,0 +1,59 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; + +import stirling.software.common.configuration.InstallationPathConfig; + +class ApplicationPropertiesDynamicYamlPropertySourceTest { + + @Test + void loads_yaml_into_environment() throws Exception { + // YAML-Config in Temp-Datei schreiben + String yaml = + "" + + "ui:\n" + + " appName: \"My App\"\n" + + "system:\n" + + " enableAnalytics: true\n"; + Path tmp = Files.createTempFile("spdf-settings-", ".yml"); + Files.writeString(tmp, yaml); + + // Pfad per statischem Mock liefern + try (MockedStatic mocked = + Mockito.mockStatic(InstallationPathConfig.class)) { + mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString()); + + ConfigurableEnvironment env = new StandardEnvironment(); + ApplicationProperties props = new ApplicationProperties(); + + props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein + + assertEquals("My App", env.getProperty("ui.appName")); + assertEquals("true", env.getProperty("system.enableAnalytics")); + } + } + + @Test + void throws_when_settings_file_missing() throws Exception { + String missing = "/path/does/not/exist/spdf.yml"; + try (MockedStatic mocked = + Mockito.mockStatic(InstallationPathConfig.class)) { + mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(missing); + + ConfigurableEnvironment env = new StandardEnvironment(); + ApplicationProperties props = new ApplicationProperties(); + + assertThrows(IOException.class, () -> props.dynamicYamlPropertySource(env)); + } + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java new file mode 100644 index 000000000..da83fd462 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java @@ -0,0 +1,248 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.ApplicationProperties.Driver; +import stirling.software.common.model.ApplicationProperties.Premium; +import stirling.software.common.model.ApplicationProperties.Security; +import stirling.software.common.model.exception.UnsupportedProviderException; + +class ApplicationPropertiesLogicTest { + + @Test + void system_isAnalyticsEnabled_null_false_true() { + ApplicationProperties.System sys = new ApplicationProperties.System(); + + sys.setEnableAnalytics(null); + assertFalse(sys.isAnalyticsEnabled()); + + sys.setEnableAnalytics(Boolean.FALSE); + assertFalse(sys.isAnalyticsEnabled()); + + sys.setEnableAnalytics(Boolean.TRUE); + assertTrue(sys.isAnalyticsEnabled()); + } + + @Test + void tempFileManagement_defaults_and_overrides() { + ApplicationProperties.TempFileManagement tfm = + new ApplicationProperties.TempFileManagement(); + + String expectedBase = + java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "") + + "/stirling-pdf"; + assertEquals(expectedBase, tfm.getBaseTmpDir()); + + String expectedLibre = expectedBase + "/libreoffice"; + assertEquals(expectedLibre, tfm.getLibreofficeDir()); + + tfm.setBaseTmpDir("/custom/base"); + assertEquals("/custom/base", tfm.getBaseTmpDir()); + + tfm.setLibreofficeDir("/opt/libre"); + assertEquals("/opt/libre", tfm.getLibreofficeDir()); + } + + @Test + void oauth2_scope_parsing_and_validity() { + Security.OAUTH2 oauth2 = new Security.OAUTH2(); + oauth2.setIssuer("https://issuer"); + oauth2.setClientId("client"); + oauth2.setClientSecret("secret"); + oauth2.setUseAsUsername("email"); + oauth2.setScopes("openid, profile ,email"); + assertTrue(oauth2.isSettingsValid()); + } + + @Test + void security_login_method_flags() { + Security sec = new Security(); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(true); + + assertTrue(sec.isUserPass()); + assertTrue(sec.isOauth2Active()); + assertTrue(sec.isSaml2Active()); + + sec.setLoginMethod(Security.LoginMethods.NORMAL.toString()); + assertTrue(sec.isUserPass()); + assertFalse(sec.isOauth2Active()); + assertFalse(sec.isSaml2Active()); + } + + @Test + void security_isAltLogin_reflects_oauth2_or_saml2() { + Security sec = new Security(); + + assertFalse(sec.isAltLogin()); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(false); + assertTrue(sec.isAltLogin()); + + sec.getOauth2().setEnabled(false); + sec.getSaml2().setEnabled(true); + assertTrue(sec.isAltLogin()); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(true); + assertTrue(sec.isAltLogin()); + } + + @Test + void oauth2_client_provider_mapping_and_unsupported() throws UnsupportedProviderException { + Security.OAUTH2.Client client = new Security.OAUTH2.Client(); + + assertNotNull(client.get("google")); + assertNotNull(client.get("github")); + assertNotNull(client.get("keycloak")); + + UnsupportedProviderException ex = + assertThrows(UnsupportedProviderException.class, () -> client.get("unknown")); + assertTrue(ex.getMessage().toLowerCase().contains("not supported")); + } + + @Test + void premium_google_drive_getters_return_empty_string_on_null_or_blank() { + Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive(); + + assertEquals("", gd.getClientId()); + assertEquals("", gd.getApiKey()); + assertEquals("", gd.getAppId()); + + gd.setClientId(" id "); + gd.setApiKey(" key "); + gd.setAppId(" app "); + assertEquals(" id ", gd.getClientId()); + assertEquals(" key ", gd.getApiKey()); + assertEquals(" app ", gd.getAppId()); + } + + @Test + void ui_getters_return_null_for_blank() { + ApplicationProperties.Ui ui = new ApplicationProperties.Ui(); + ui.setAppName(" "); + ui.setHomeDescription(""); + ui.setAppNameNavbar(null); + + assertNull(ui.getAppName()); + assertNull(ui.getHomeDescription()); + assertNull(ui.getAppNameNavbar()); + + ui.setAppName("Stirling-PDF"); + ui.setHomeDescription("Home"); + ui.setAppNameNavbar("Nav"); + assertEquals("Stirling-PDF", ui.getAppName()); + assertEquals("Home", ui.getHomeDescription()); + assertEquals("Nav", ui.getAppNameNavbar()); + } + + @Test + void driver_toString_contains_driver_name() { + assertTrue(Driver.H2.toString().contains("h2")); + assertTrue(Driver.POSTGRESQL.toString().contains("postgresql")); + } + + @Test + void session_limits_and_timeouts_have_reasonable_defaults() { + ApplicationProperties.ProcessExecutor pe = new ApplicationProperties.ProcessExecutor(); + + ApplicationProperties.ProcessExecutor.SessionLimit s = pe.getSessionLimit(); + assertEquals(2, s.getQpdfSessionLimit()); + assertEquals(1, s.getTesseractSessionLimit()); + assertEquals(1, s.getLibreOfficeSessionLimit()); + assertEquals(1, s.getPdfToHtmlSessionLimit()); + assertEquals(8, s.getPythonOpenCvSessionLimit()); + assertEquals(16, s.getWeasyPrintSessionLimit()); + assertEquals(1, s.getInstallAppSessionLimit()); + assertEquals(1, s.getCalibreSessionLimit()); + assertEquals(8, s.getGhostscriptSessionLimit()); + assertEquals(2, s.getOcrMyPdfSessionLimit()); + + ApplicationProperties.ProcessExecutor.TimeoutMinutes t = pe.getTimeoutMinutes(); + assertEquals(30, t.getTesseractTimeoutMinutes()); + assertEquals(30, t.getQpdfTimeoutMinutes()); + assertEquals(30, t.getLibreOfficeTimeoutMinutes()); + assertEquals(20, t.getPdfToHtmlTimeoutMinutes()); + assertEquals(30, t.getPythonOpenCvTimeoutMinutes()); + assertEquals(30, t.getWeasyPrintTimeoutMinutes()); + assertEquals(60, t.getInstallAppTimeoutMinutes()); + assertEquals(30, t.getCalibreTimeoutMinutes()); + assertEquals(30, t.getGhostscriptTimeoutMinutes()); + assertEquals(30, t.getOcrMyPdfTimeoutMinutes()); + } + + @Deprecated + @Test + void enterprise_metadata_defaults() { + ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition(); + ApplicationProperties.EnterpriseEdition.CustomMetadata eMeta = ee.getCustomMetadata(); + eMeta.setCreator(" "); + eMeta.setProducer(null); + assertEquals("Stirling-PDF", eMeta.getCreator()); + assertEquals("Stirling-PDF", eMeta.getProducer()); + } + + @Test + void premium_metadata_defaults() { + Premium.ProFeatures pf = new Premium.ProFeatures(); + Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata(); + pMeta.setCreator(""); + pMeta.setProducer(""); + assertEquals("Stirling-PDF", pMeta.getCreator()); + assertEquals("Stirling-PDF", pMeta.getProducer()); + } + + @Test + void premium_metadata_awesome() { + Premium.ProFeatures pf = new Premium.ProFeatures(); + Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata(); + pMeta.setCreator("Awesome PDF Tool"); + pMeta.setProducer("Awesome PDF Tool"); + assertEquals("Awesome PDF Tool", pMeta.getCreator()); + assertEquals("Awesome PDF Tool", pMeta.getProducer()); + } + + @Test + void string_isValid_handles_null_empty_blank_and_trimmed() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + assertFalse(oauth2.isValid((String) null, "issuer")); + assertFalse(oauth2.isValid("", "issuer")); + assertFalse(oauth2.isValid(" ", "issuer")); + + assertTrue(oauth2.isValid("x", "issuer")); + assertTrue(oauth2.isValid(" x ", "issuer")); // trimmt intern + } + + @Test + void collection_isValid_handles_null_and_empty() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + Collection nullColl = null; + Collection empty = List.of(); + + assertFalse(oauth2.isValid(nullColl, "scopes")); + assertFalse(oauth2.isValid(empty, "scopes")); + } + + @Test + void collection_isValid_true_when_non_empty_even_if_element_is_blank() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + // Aktuelles Verhalten: prüft NUR !isEmpty(), nicht Inhalt + Collection oneBlank = new ArrayList<>(); + oneBlank.add(" "); + + assertTrue( + oauth2.isValid(oneBlank, "scopes"), + "Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn Element leer/blank ist"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java new file mode 100644 index 000000000..3fa8299ca --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java @@ -0,0 +1,80 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; + +class ApplicationPropertiesSaml2HttpTest { + + @Test + void idpMetadataUri_http_is_resolved_via_mockwebserver() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue( + new MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/xml") + .setBody("")); + server.start(); + + String url = server.url("/meta").toString(); + + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri(url); + + try (InputStream in = s.getIdpMetadataUri()) { + String body = new String(in.readAllBytes(), StandardCharsets.UTF_8); + assertTrue(body.contains("EntityDescriptor")); + } + } + } + + @Test + void idpMetadataUri_invalidUri_triggers_catch_and_throwsIOException() { + // Ungültige URI -> new URI(...) wirft URISyntaxException -> catch -> IOException + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##) + + assertThrows(IOException.class, s::getIdpMetadataUri); + } + + @Test + void spCert_else_branch_returns_FileSystemResource_for_filesystem_path() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + + // temporäre Datei simuliert "Filesystem"-Pfad (-> else-Zweig) + Path tmp = Files.createTempFile("spdf-spcert-", ".crt"); + Files.writeString(tmp, "CERT"); + + s.setSpCert(tmp.toString()); + Resource r = s.getSpCert(); + + assertNotNull(r); + assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path"); + assertTrue(r.exists(), "Temp file should exist"); + } + + @Test + void idpCert_else_branch_returns_FileSystemResource_even_if_missing() { + var s = new ApplicationProperties.Security.SAML2(); + + // bewusst nicht existierender Pfad -> else-Zweig wird trotzdem genommen + String missing = "/this/path/does/not/exist/idp.crt"; + s.setIdpCert(missing); + Resource r = s.getIdpCert(); + + assertNotNull(r); + assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path"); + assertFalse(r.exists(), "Resource should not exist for missing file"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java new file mode 100644 index 000000000..efc266561 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java @@ -0,0 +1,55 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.Resource; + +class ApplicationPropertiesSaml2ResourceTest { + + @Test + void idpMetadataUri_classpath_is_resolved() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri("classpath:saml/dummy.txt"); + + try (InputStream in = s.getIdpMetadataUri()) { + assertNotNull(in, "Classpath InputStream should not be null"); + String txt = new String(in.readAllBytes(), StandardCharsets.UTF_8); + assertTrue(txt.contains("ok")); + } + } + + @Test + void spCert_idpCert_privateKey_null_classpath_and_filesystem() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + + s.setSpCert(null); + s.setIdpCert(null); + s.setPrivateKey(null); + assertNull(s.getSpCert()); + assertNull(s.getIdpCert()); + assertNull(s.getPrivateKey()); + + s.setSpCert("classpath:saml/dummy.txt"); + s.setIdpCert("classpath:saml/dummy.txt"); + s.setPrivateKey("classpath:saml/dummy.txt"); + Resource sp = s.getSpCert(); + Resource idp = s.getIdpCert(); + Resource pk = s.getPrivateKey(); + assertTrue(sp.exists()); + assertTrue(idp.exists()); + assertTrue(pk.exists()); + + Path tmp = Files.createTempFile("spdf-key-", ".pem"); + Files.writeString(tmp, "KEY"); + s.setPrivateKey(tmp.toString()); + Resource pkFs = s.getPrivateKey(); + assertNotNull(pkFs); + assertTrue(pkFs.exists()); + } +} diff --git a/app/common/src/test/resources/saml/dummy.txt b/app/common/src/test/resources/saml/dummy.txt new file mode 100644 index 000000000..9766475a4 --- /dev/null +++ b/app/common/src/test/resources/saml/dummy.txt @@ -0,0 +1 @@ +ok diff --git a/build.gradle b/build.gradle index e54c58e7d..2c151d11b 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ allprojects { tasks.register('writeVersion', WriteProperties) { outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties') println "Writing version.properties to ${outputFile.path}" - comment "${new Date()}" + comment = "${new Date()}" property 'version', project.provider { project.version.toString() } } @@ -128,6 +128,9 @@ subprojects { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" + + testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0") + testImplementation "com.squareup.okhttp3:mockwebserver" } tasks.withType(JavaCompile).configureEach { @@ -153,6 +156,17 @@ subprojects { } } + jacocoTestCoverageVerification { + dependsOn jacocoTestReport + violationRules { + rule { + limit { + minimum = 0.0 + } + } + } + } + tasks.named("processResources") { dependsOn(rootProject.tasks.writeVersion) } @@ -569,6 +583,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" + + testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0") + testImplementation "com.squareup.okhttp3:mockwebserver" } tasks.named("test") { From dd0bf194cda86b3a783b04f6b07b6fe9906b935f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:31:28 +0200 Subject: [PATCH 66/71] Update Hungarian translation for new update related strings (#4152) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../main/resources/messages_hu_HU.properties | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 45de2334c..c5488bc2b 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -368,36 +368,36 @@ settings.update=Frissítés elérhető settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Sürgős frissítés érhető el +update.updateAvailable=Frissítés érhető el +update.modalTitle=Frissítés érhető el +update.current=Jelenlegi verzió +update.latest=Legújabb verzió +update.latestStable=Legújabb stabil verzió +update.priority=Fontosság +update.recommendedAction=Ajánlott lépés +update.breakingChangesDetected=⚠️ Jelentős változások észlelve +update.breakingChangesMessage=Ez a frissítés jelentős változásokat tartalmaz. Kérjük, olvassa el az alábbi migrációs útmutatót. +update.migrationGuides=Migrációs útmutatók: +update.viewGuide=Útmutató megtekintése +update.loadingDetailedInfo=Részletes verzióinformációk betöltése folyamatban... +update.close=Bezárás +update.viewAllReleases=Összes kiadás megtekintése +update.downloadLatest=Legújabb verzió letöltése +update.availableUpdates=Elérhető frissítések: +update.unableToLoadDetails=Nem sikerült betölteni a részletes verzióinformációkat. +update.version=Verzió # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=SÜRGETŐ +update.priority.normal=NORMÁL +update.priority.minor=KISEBB +update.priority.low=ALACSONY # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Megszakító változások: +update.breakingChangesDefault=Ez a verzió megszakító változásokat tartalmaz +update.migrationGuide=Migrációs útmutató settings.appVersion=Alkalmazás verziója: settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén): settings.downloadOption.1=Megnyitás ugyanabban az ablakban From 979f30227736361294a3744ad95178d7115b01e7 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:33:08 +0100 Subject: [PATCH 67/71] :globe_with_meridians: Sync Translations + Update README Progress Table (#4159) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e212780a..0533376b8 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Stirling-PDF currently supports 40 languages! | German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) | | Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) | | Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) | -| Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | +| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | | Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | | Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | From 7c49379a70171bbe51af4dca16c07a57b475d5a9 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Sun, 10 Aug 2025 17:49:16 +0100 Subject: [PATCH 68/71] remove old dockers --- Dockerfile | 101 ------------------------------------- Dockerfile.dev | 60 ---------------------- Dockerfile.fat | 113 ------------------------------------------ Dockerfile.ultra-lite | 54 -------------------- 4 files changed, 328 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Dockerfile.dev delete mode 100644 Dockerfile.fat delete mode 100644 Dockerfile.ultra-lite diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 375ab94c1..000000000 --- a/Dockerfile +++ /dev/null @@ -1,101 +0,0 @@ -# Main stage -FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 - -# Copy necessary files -COPY scripts /scripts -COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ -COPY app/core/build/libs/*.jar app.jar - -ARG VERSION_TAG - -LABEL org.opencontainers.image.title="Stirling-PDF" -LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more." -LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF" -LABEL org.opencontainers.image.licenses="MIT" -LABEL org.opencontainers.image.vendor="Stirling-Tools" -LABEL org.opencontainers.image.url="https://www.stirlingpdf.com" -LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com" -LABEL maintainer="Stirling-Tools" -LABEL org.opencontainers.image.authors="Stirling-Tools" -LABEL org.opencontainers.image.version="${VERSION_TAG}" -LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark" - -# Set Environment Variables -ENV DISABLE_ADDITIONAL_FEATURES=true \ - VERSION_TAG=$VERSION_TAG \ - JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ - JAVA_CUSTOM_OPTS="" \ - HOME=/home/stirlingpdfuser \ - PUID=1000 \ - PGID=1000 \ - UMASK=022 \ - PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ - UNO_PATH=/usr/lib/libreoffice/program \ - URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ - PATH=$PATH:/opt/venv/bin \ - STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ - TMPDIR=/tmp/stirling-pdf \ - TEMP=/tmp/stirling-pdf \ - TMP=/tmp/stirling-pdf - - -# JDK for app -RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ - apk upgrade --no-cache -a && \ - apk add --no-cache \ - ca-certificates \ - tzdata \ - tini \ - bash \ - curl \ - shadow \ - su-exec \ - openssl \ - openssl-dev \ - openjdk21-jre \ - # Doc conversion - gcompat \ - libc6-compat \ - libreoffice \ - # pdftohtml - poppler-utils \ - # OCR MY PDF (unpaper for descew and other advanced features) - tesseract-ocr-data-eng \ - tesseract-ocr-data-chi_sim \ - tesseract-ocr-data-deu \ - tesseract-ocr-data-fra \ - tesseract-ocr-data-por \ - unpaper \ - # CV - py3-opencv \ - python3 \ - ocrmypdf \ - py3-pip \ - py3-pillow@testing \ - py3-pdf2image@testing \ - # URW Base 35 fonts for better PDF rendering - font-urw-base35 && \ - python3 -m venv /opt/venv && \ - /opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \ - /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ - ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ - ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ - ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ - mv /usr/share/tessdata /usr/share/tessdata-original && \ - mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ - # Configure URW Base 35 fonts - ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \ - fc-cache -f -v && \ - chmod +x /scripts/* && \ - # User permissions - addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ - chown stirlingpdfuser:stirlingpdfgroup /app.jar - -EXPOSE 8080/tcp - -# Set user and run command -ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 48084878d..000000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,60 +0,0 @@ -# dockerfile.dev - -# Basisimage: Gradle mit JDK 17 (Debian-basiert) -FROM gradle:8.14-jdk17 - -# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren -USER root - -# Set GRADLE_HOME und füge Gradle zum PATH hinzu -ENV GRADLE_HOME=/opt/gradle -ENV PATH="$GRADLE_HOME/bin:$PATH" - -# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert) -RUN apt-get update && apt-get install -y \ - sudo \ - libreoffice \ - poppler-utils \ - qpdf \ -# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata - tesseract-ocr \ - tesseract-ocr-eng \ - fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \ - python3-uno \ - python3-venv \ -# ss -tln - iproute2 \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Setze die Environment Variable für setuptools -ENV SETUPTOOLS_USE_DISTUTILS=local \ - STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ - TMPDIR=/tmp/stirling-pdf \ - TEMP=/tmp/stirling-pdf \ - TMP=/tmp/stirling-pdf - -# Installation der benötigten Python-Pakete -RUN python3 -m venv --system-site-packages /opt/venv \ - && . /opt/venv/bin/activate \ - && pip install --no-cache-dir --upgrade pip setuptools \ - && pip install --no-cache-dir WeasyPrint pdf2image pillow unoserver opencv-python-headless pre-commit - -# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind -ENV PATH="/opt/venv/bin:$PATH" - -COPY . /workspace - -RUN mkdir -p /tmp/stirling-pdf \ - && fc-cache -f -v \ - && adduser --disabled-password --gecos '' devuser \ - && chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf -RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \ - && chmod 0440 /etc/sudoers.d/devuser - -# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben) -WORKDIR /workspace - -RUN chmod +x /workspace/.devcontainer/git-init.sh /workspace/.devcontainer/init-setup.sh - -# Wechsel zum Nicht‑Root Benutzer -USER devuser diff --git a/Dockerfile.fat b/Dockerfile.fat deleted file mode 100644 index fda3d89c4..000000000 --- a/Dockerfile.fat +++ /dev/null @@ -1,113 +0,0 @@ -# Build the application -FROM gradle:8.14-jdk21 AS build - -COPY build.gradle . -COPY settings.gradle . -COPY gradlew . -COPY gradle gradle/ -COPY app/core/build.gradle core/. -COPY app/common/build.gradle common/. -COPY app/proprietary/build.gradle proprietary/. -RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 - -# Set the working directory -WORKDIR /app - -# Copy the entire project to the working directory -COPY . . - -# Build the application with DISABLE_ADDITIONAL_FEATURES=false -RUN DISABLE_ADDITIONAL_FEATURES=false \ - STIRLING_PDF_DESKTOP_UI=false \ - ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube - -# Main stage -FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 - -# Copy necessary files -COPY scripts /scripts -COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ -# first /app directory is for the build stage, second is for the final image -COPY --from=build /app/app/core/build/libs/*.jar app.jar - -ARG VERSION_TAG - -# Set Environment Variables -ENV DISABLE_ADDITIONAL_FEATURES=true \ - VERSION_TAG=$VERSION_TAG \ - JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ - JAVA_CUSTOM_OPTS="" \ - HOME=/home/stirlingpdfuser \ - PUID=1000 \ - PGID=1000 \ - UMASK=022 \ - FAT_DOCKER=true \ - INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ - PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ - UNO_PATH=/usr/lib/libreoffice/program \ - URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ - PATH=$PATH:/opt/venv/bin \ - STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ - TMPDIR=/tmp/stirling-pdf \ - TEMP=/tmp/stirling-pdf \ - TMP=/tmp/stirling-pdf - - -# JDK for app -RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ - apk upgrade --no-cache -a && \ - apk add --no-cache \ - ca-certificates \ - tzdata \ - tini \ - bash \ - curl \ - shadow \ - su-exec \ - openssl \ - openssl-dev \ - openjdk21-jre \ - # Doc conversion - gcompat \ - libc6-compat \ - libreoffice \ - # pdftohtml - poppler-utils \ - # OCR MY PDF (unpaper for descew and other advanced featues) - tesseract-ocr-data-eng \ - tesseract-ocr-data-chi_sim \ - tesseract-ocr-data-deu \ - tesseract-ocr-data-fra \ - tesseract-ocr-data-por \ - unpaper \ - font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \ - # CV - py3-opencv \ - python3 \ - ocrmypdf \ - py3-pip \ - py3-pillow@testing \ - py3-pdf2image@testing && \ - python3 -m venv /opt/venv && \ - /opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \ - /opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ - ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \ - ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ - ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ - mv /usr/share/tessdata /usr/share/tessdata-original && \ - mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ - # Configure URW Base 35 fonts - ln -s /usr/share/fontconfig/conf.avail/69-urw-*.conf /etc/fonts/conf.d/ && \ - fc-cache -f -v && \ - chmod +x /scripts/* && \ - # User permissions - addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ - chown stirlingpdfuser:stirlingpdfgroup /app.jar - -EXPOSE 8080/tcp -# Set user and run command -ENTRYPOINT ["tini", "--", "/scripts/init.sh"] -CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -Djava.io.tmpdir=/tmp/stirling-pdf -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"] diff --git a/Dockerfile.ultra-lite b/Dockerfile.ultra-lite deleted file mode 100644 index acc62b93b..000000000 --- a/Dockerfile.ultra-lite +++ /dev/null @@ -1,54 +0,0 @@ -# use alpine -FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 - -ARG VERSION_TAG - -# Set Environment Variables -ENV DISABLE_ADDITIONAL_FEATURES=true \ - HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG \ - JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \ - JAVA_CUSTOM_OPTS="" \ - PUID=1000 \ - PGID=1000 \ - UMASK=022 \ - STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \ - TMPDIR=/tmp/stirling-pdf \ - TEMP=/tmp/stirling-pdf \ - TMP=/tmp/stirling-pdf - -# Copy necessary files -COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh -COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh -COPY scripts/installFonts.sh /scripts/installFonts.sh -COPY app/core/build/libs/*.jar app.jar - -# Set up necessary directories and permissions -RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ - echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ - apk upgrade --no-cache -a && \ - apk add --no-cache \ - ca-certificates \ - tzdata \ - tini \ - bash \ - curl \ - shadow \ - su-exec \ - openjdk21-jre && \ - # User permissions - mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ - chmod +x /scripts/*.sh && \ - addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /tmp/stirling-pdf && \ - chown stirlingpdfuser:stirlingpdfgroup /app.jar - -# Set environment variables -ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI - -EXPOSE 8080/tcp - -# Run the application -ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"] -CMD ["java", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=/tmp/stirling-pdf", "-jar", "/app.jar"] From af69256abb41be605e75841cfe7a78f807e20901 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Sun, 10 Aug 2025 18:15:05 +0100 Subject: [PATCH 69/71] docker fixes --- docker/backend/Dockerfile | 5 ++--- docker/backend/Dockerfile.fat | 5 ++--- docker/backend/Dockerfile.ultra-lite | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 0178509e9..58655dfdb 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -27,7 +27,6 @@ FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8 # Copy necessary files COPY scripts /scripts -COPY pipeline /pipeline COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ # first /app directory is for the build stage, second is for the final image COPY --from=build /app/app/core/build/libs/*.jar app.jar @@ -107,13 +106,13 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ mv /usr/share/tessdata /usr/share/tessdata-original && \ - mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ fc-cache -f -v && \ chmod +x /scripts/* && \ chmod +x /scripts/init.sh && \ # User permissions addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ chown stirlingpdfuser:stirlingpdfgroup /app.jar EXPOSE 8080/tcp diff --git a/docker/backend/Dockerfile.fat b/docker/backend/Dockerfile.fat index 33468953b..bd12e3063 100644 --- a/docker/backend/Dockerfile.fat +++ b/docker/backend/Dockerfile.fat @@ -27,7 +27,6 @@ FROM alpine:3.22.1@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8 # Copy necessary files COPY scripts /scripts -COPY pipeline /pipeline COPY app/core/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ # first /app directory is for the build stage, second is for the final image COPY --from=build /app/app/core/build/libs/*.jar app.jar @@ -98,13 +97,13 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \ ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ mv /usr/share/tessdata /usr/share/tessdata-original && \ - mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf && \ + mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ fc-cache -f -v && \ chmod +x /scripts/* && \ chmod +x /scripts/init.sh && \ # User permissions addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \ chown stirlingpdfuser:stirlingpdfgroup /app.jar EXPOSE 8080/tcp diff --git a/docker/backend/Dockerfile.ultra-lite b/docker/backend/Dockerfile.ultra-lite index 426d5b410..0b74e3b0a 100644 --- a/docker/backend/Dockerfile.ultra-lite +++ b/docker/backend/Dockerfile.ultra-lite @@ -44,7 +44,6 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \ # Copy necessary files COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh COPY scripts/installFonts.sh /scripts/installFonts.sh -COPY pipeline /pipeline COPY --from=build /app/app/core/build/libs/*.jar app.jar # Set up necessary directories and permissions @@ -62,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et su-exec \ openjdk21-jre && \ # User permissions - mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf && \ + mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto /tmp/stirling-pdf /pipeline/watchedFolders /pipeline/finishedFolders && \ chmod +x /scripts/*.sh && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ - chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline /tmp/stirling-pdf && \ + chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /pipeline /tmp/stirling-pdf && \ chown stirlingpdfuser:stirlingpdfgroup /app.jar # Set environment variables From 17d9993a53f8c1f0add8b2464b6c33c5f9dfb49c Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Mon, 11 Aug 2025 10:22:47 +0100 Subject: [PATCH 70/71] fix merge issues --- .github/workflows/build.yml | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d28b669c..2642404cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,11 +52,7 @@ jobs: spring-security: [true, false] steps: - name: Harden Runner -<<<<<<< HEAD uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 -======= - uses: step-security/harden-runner@v2.12.2 ->>>>>>> refs/remotes/origin/V2 with: egress-policy: audit - name: Checkout repository @@ -110,11 +106,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner -<<<<<<< HEAD - uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 -======= uses: step-security/harden-runner@v2.12.2 ->>>>>>> refs/remotes/origin/V2 with: egress-policy: audit - uses: actions/checkout@v4.2.2 @@ -126,10 +118,7 @@ jobs: - uses: gradle/actions/setup-gradle@v4.4.1 - name: Generate OpenAPI documentation run: ./gradlew :stirling-pdf:generateOpenApiDocs -<<<<<<< HEAD -======= ->>>>>>> refs/remotes/origin/V2 - name: Upload OpenAPI Documentation uses: actions/upload-artifact@v4.6.2 with: @@ -170,11 +159,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner -<<<<<<< HEAD uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 -======= - uses: step-security/harden-runner@v2.12.2 ->>>>>>> refs/remotes/origin/V2 with: egress-policy: audit - name: Checkout repository @@ -253,7 +238,6 @@ jobs: chmod +x ./testing/test.sh chmod +x ./testing/test_disabledEndpoints.sh ./testing/test.sh -<<<<<<< HEAD test-build-docker-images: if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true' @@ -320,5 +304,3 @@ jobs: build/reports/problems/ retention-days: 3 if-no-files-found: warn -======= ->>>>>>> refs/remotes/origin/V2 From c5328d2aee8cd64971a42b55af8bccd0336bb6bb Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:30:05 +0100 Subject: [PATCH 71/71] Update build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2642404cb..453c4f445 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -285,7 +285,7 @@ jobs: with: builder: ${{ steps.buildx.outputs.name }} context: . - file: ./${{ matrix.docker-rev }} + file: ./docker/backend/${{ matrix.docker-rev }} push: false cache-from: type=gha cache-to: type=gha,mode=max