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/77] 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 745dbb87a3..ca7a007b78 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/77] 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 61c1dcc774..fe427fea95 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 cdf2ba5145..87cb5121cd 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 1e6219a85c..85a9ab0ca3 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/77] 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 877a78524f..013db2886f 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 855e804b27..29aea43894 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 b9fd7c2774..7c47b8d583 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 bf290de76a..bd998d1977 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 d5b637899f..cdca40e0b2 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 da000201a2..9fac8bde00 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 154b6bdaec..30c96a1b06 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 23c15816f0..dc6503c27f 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 15349a66db..1388ef0fb6 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 3cac33e1fb..6f615417f0 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 ba80e9bcdb..c4697a9657 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 432925f1af..c6f3b1c6bc 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 701bb678eb..85790f47b6 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 948a5a37b5..eca90c9b84 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 f708a5b8de..b994d93387 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 237040f0a4..88b150e290 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 463736b657..e038f699e6 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 620209dbb4..dbcf7b1dac 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 85c93a2440..0143cea81c 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/77] 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 eca90c9b84..120a223ad1 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/77] 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 cdca40e0b2..c38571abb9 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/77] 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 6f615417f0..b55c7d4027 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 c6f3b1c6bc..47cb401823 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 85790f47b6..ba970e8855 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/77] 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 0c62a0e07a..d97911bbe3 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/77] 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 d97911bbe3..1e472e083d 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/77] 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 e4edf2baa6..91b3287592 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 0000000000..97c2da12eb --- /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 e5fe0436a6..05d9b73a61 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 05e9cec5cc..6b28dc6839 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 c735e5287c..799f91e050 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 65bffe05e8..59e5f81b1e 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 c37be9b847..fb8911eaf7 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/77] 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 627d7b5c17..fd9abf7c8c 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 9184cf5c63..8a390f5927 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/77] 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 8d4e98e5a4..7d5389fda8 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/77] :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 b0a563fa52..b9660ce432 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/77] 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 9531a359b5..3c01bcbd60 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 86e65cd013..0b64bb6799 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/77] 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 013db2886f..066d85ef27 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 dbbc2622da..2a04ba33e8 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 cdb8b345d0..b5759ed542 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/77] 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 b7d944c34a..8a2e8b8ef8 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/77] 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 5ab9f82c9d..dcc0ca6004 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 d45455a7ac..3f5158dead 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 2f4f242cbd..225470ea99 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 9b35ccb1a9..b6cd969f64 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 abc54d43e6..5b8f77bbc0 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 fd9abf7c8c..ec786e2edc 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/77] 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 0000000000..0815b1c567 --- /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 0000000000..9acc30c164 --- /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 6b28dc6839..85005af401 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 0000000000..2478aad945 --- /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/77] 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 37be2c06a2..f619b7b6eb 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 2aef90d8c3..ffc22ed083 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 e5aea93452..833d4fd916 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 ec786e2edc..39672cf24f 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/77] =?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 b6cd969f64..a79fb8be59 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 ffc22ed083..5dee332386 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/77] :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 b9660ce432..3b582cbfc3 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 71bedd8e29..1cd554cd18 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 151dc0e641..2304a13d17 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 63b0c0b85e..a99e9447ee 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 5b39cdcf53..aef66f1285 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 a8f9a560f0..ff7f2b64bd 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 a83268aa2a..a68fbcb789 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 bc06c09155..8d55cc8d17 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 1bb923450d..63b54fa741 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 e4209faf81..a9fbee5387 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 e6bad97d04..877c25e758 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 40fe589876..4ccb6d7588 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 92bb97c63b..513f5241ed 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 02e44b563f..dccb7fc0be 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 f45f94078f..7f53edbfe9 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 874c8ebca6..816932ff13 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 369d9444cc..e2f9b2c19a 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 87a4add1d4..7ea02b909e 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 490dbeccea..45de2334c6 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 4709453728..541226f692 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 71c0f9ffc5..0db465a405 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 fdffa35231..ced0c7c56c 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 b129e9c699..7de79d52cd 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 775b68792f..123f5a53f9 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 94b1bb0204..44418eb0f0 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 dadc0bc320..ed830ec3f7 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 7d553c5746..0eefb4ccc7 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 cde839e5e5..57e8dd93e1 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 49998f2730..2c78fa93b3 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 e33d01f4a7..5a904a9c8d 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 072e031238..4580f39339 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 10ed3d9851..68faeab85b 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 8b15dcc427..fe95a41655 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 305b68aa1d..f15d8397a7 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 e731f6337f..7a786add64 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 7a2b20aea7..9b332982c6 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 c03d7872e1..72e78f1b32 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 f24d997ac8..db5739fe38 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 cd2e412f7e..0a1e9b3922 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 252eb27680..4eeac64833 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 9f38178ac6..cee6b9c7d0 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/77] 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 ae6c0b66f4..fd4091d4c0 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 037a894972..c9905a308e 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 f619b7b6eb..d6056e856b 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 877c25e758..250dd51c5c 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 513f5241ed..27dbfdb087 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 7f53edbfe9..86e6c0d956 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 ced0c7c56c..a5af895fd8 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 6c37e64736..c3b01eec2b 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 39672cf24f..e54c58e7d4 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/77] =?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 fd4091d4c0..7140b3cc22 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/77] 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 59acd0fc2e..23278a23fe 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 b45d3a43d4fd63b3dc504433cd36c9f808d76aeb Mon Sep 17 00:00:00 2001 From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:56:20 +0100 Subject: [PATCH 61/77] V2 Restructure homepage (#4138) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Component Extraction & Context Refactor - Summary 🔧 What We Did - Extracted HomePage's 286-line monolithic component into focused parts - Created ToolPanel (105 lines) for tool selection UI - Created Workbench (203 lines) for view management - Created ToolWorkflowContext (220 lines) for centralized state - Reduced HomePage to 60 lines of provider setup - Eliminated all prop drilling - components use contexts directly 🏆 Why This is Good - Maintainability: Each component has single purpose, easy debugging/development - Architecture: Clean separation of concerns, future features easier to add - Code Quality: 105% more lines but organized/purposeful vs tangled spaghetti code --------- Co-authored-by: Connor Yoh Co-authored-by: James Brunton --- frontend/src/components/layout/Workbench.tsx | 160 +++++++++++ .../src/components/shared/QuickAccessBar.tsx | 25 +- frontend/src/components/tools/ToolPanel.tsx | 89 ++++++ frontend/src/components/tools/ToolPicker.tsx | 25 +- frontend/src/contexts/ToolWorkflowContext.tsx | 221 +++++++++++++++ frontend/src/pages/HomePage.tsx | 265 ++---------------- frontend/src/types/sidebar.ts | 6 - 7 files changed, 503 insertions(+), 288 deletions(-) create mode 100644 frontend/src/components/layout/Workbench.tsx create mode 100644 frontend/src/components/tools/ToolPanel.tsx create mode 100644 frontend/src/contexts/ToolWorkflowContext.tsx diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx new file mode 100644 index 0000000000..b0c984ee88 --- /dev/null +++ b/frontend/src/components/layout/Workbench.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { Box } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; +import { useWorkbenchState, useToolSelection } from '../../contexts/ToolWorkflowContext'; +import { useFileHandler } from '../../hooks/useFileHandler'; +import { useFileContext } from '../../contexts/FileContext'; + +import TopControls from '../shared/TopControls'; +import FileEditor from '../fileEditor/FileEditor'; +import PageEditor from '../pageEditor/PageEditor'; +import PageEditorControls from '../pageEditor/PageEditorControls'; +import Viewer from '../viewer/Viewer'; +import ToolRenderer from '../tools/ToolRenderer'; +import LandingPage from '../shared/LandingPage'; + +// No props needed - component uses contexts directly +export default function Workbench() { + const { t } = useTranslation(); + const { isRainbowMode } = useRainbowThemeContext(); + + // Use context-based hooks to eliminate all prop drilling + const { activeFiles, currentView, setCurrentView } = useFileContext(); + const { + previewFile, + pageEditorFunctions, + sidebarsVisible, + setPreviewFile, + setPageEditorFunctions, + setSidebarsVisible + } = useWorkbenchState(); + + const { selectedToolKey, selectedTool, handleToolSelect } = useToolSelection(); + const { addToActiveFiles } = useFileHandler(); + + const handlePreviewClose = () => { + setPreviewFile(null); + const previousMode = sessionStorage.getItem('previousMode'); + if (previousMode === 'split') { + // Use context's handleToolSelect which coordinates tool selection and view changes + handleToolSelect('split'); + sessionStorage.removeItem('previousMode'); + } else if (previousMode === 'compress') { + handleToolSelect('compress'); + sessionStorage.removeItem('previousMode'); + } else if (previousMode === 'convert') { + handleToolSelect('convert'); + sessionStorage.removeItem('previousMode'); + } else { + setCurrentView('fileEditor' as any); + } + }; + + const renderMainContent = () => { + if (!activeFiles[0]) { + return ( + + ); + } + + switch (currentView) { + case "fileEditor": + return ( + { + setCurrentView("pageEditor" as any); + }, + onMergeFiles: (filesToMerge) => { + filesToMerge.forEach(addToActiveFiles); + setCurrentView("viewer" as any); + } + })} + /> + ); + + case "viewer": + return ( + + ); + + case "pageEditor": + return ( + <> + + {pageEditorFunctions && ( + + )} + + ); + + default: + // Check if it's a tool view + if (selectedToolKey && selectedTool) { + return ( + + ); + } + return ( + + ); + } + }; + + return ( + + {/* Top Controls */} + + + {/* Main content area */} + + {renderMainContent()} + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx index fb27b1c2c7..7aed3632b3 100644 --- a/frontend/src/components/shared/QuickAccessBar.tsx +++ b/frontend/src/components/shared/QuickAccessBar.tsx @@ -11,20 +11,18 @@ import { useRainbowThemeContext } from "./RainbowThemeProvider"; import AppConfigModal from './AppConfigModal'; import { useIsOverflowing } from '../../hooks/useIsOverflowing'; import { useFilesModalContext } from '../../contexts/FilesModalContext'; -import { QuickAccessBarProps, ButtonConfig } from '../../types/sidebar'; +import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; +import { ButtonConfig } from '../../types/sidebar'; import './QuickAccessBar.css'; function NavHeader({ activeButton, - setActiveButton, - onReaderToggle, - onToolsClick + setActiveButton }: { activeButton: string; setActiveButton: (id: string) => void; - onReaderToggle: () => void; - onToolsClick: () => void; }) { + const { handleReaderToggle, handleBackToTools } = useToolWorkflow(); return ( <>
@@ -60,8 +58,8 @@ function NavHeader({ variant="subtle" onClick={() => { setActiveButton('tools'); - onReaderToggle(); - onToolsClick(); + handleReaderToggle(); + handleBackToTools(); }} style={{ backgroundColor: activeButton === 'tools' ? 'var(--icon-tools-bg)' : 'var(--icon-inactive-bg)', @@ -84,12 +82,11 @@ function NavHeader({ ); } -const QuickAccessBar = forwardRef(({ - onToolsClick, - onReaderToggle, +const QuickAccessBar = forwardRef(({ }, ref) => { const { isRainbowMode } = useRainbowThemeContext(); const { openFilesModal, isFilesModalOpen } = useFilesModalContext(); + const { handleReaderToggle } = useToolWorkflow(); const [configModalOpen, setConfigModalOpen] = useState(false); const [activeButton, setActiveButton] = useState('tools'); const scrollableRef = useRef(null); @@ -110,7 +107,7 @@ const QuickAccessBar = forwardRef(({ type: 'navigation', onClick: () => { setActiveButton('read'); - onReaderToggle(); + handleReaderToggle(); } }, { @@ -218,9 +215,7 @@ const QuickAccessBar = forwardRef(({
diff --git a/frontend/src/components/tools/ToolPanel.tsx b/frontend/src/components/tools/ToolPanel.tsx new file mode 100644 index 0000000000..1551ea6c99 --- /dev/null +++ b/frontend/src/components/tools/ToolPanel.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { TextInput } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useRainbowThemeContext } from '../shared/RainbowThemeProvider'; +import { useToolPanelState, useToolSelection, useWorkbenchState } from '../../contexts/ToolWorkflowContext'; +import ToolPicker from './ToolPicker'; +import ToolRenderer from './ToolRenderer'; +import { useSidebarContext } from "../../contexts/SidebarContext"; +import rainbowStyles from '../../styles/rainbow.module.css'; + +// No props needed - component uses context + +export default function ToolPanel() { + const { t } = useTranslation(); + const { isRainbowMode } = useRainbowThemeContext(); + const { sidebarRefs } = useSidebarContext(); + const { toolPanelRef } = sidebarRefs; + + + // Use context-based hooks to eliminate prop drilling + const { + leftPanelView, + isPanelVisible, + searchQuery, + filteredTools, + setSearchQuery, + handleBackToTools + } = useToolPanelState(); + + const { selectedToolKey, handleToolSelect } = useToolSelection(); + const { setPreviewFile } = useWorkbenchState(); + + return ( +
+
+ {/* Search Bar - Always visible at the top */} +
+ setSearchQuery(e.currentTarget.value)} + autoComplete="off" + size="sm" + /> +
+ + {leftPanelView === 'toolPicker' ? ( + // Tool Picker View +
+ +
+ ) : ( + // Selected Tool Content View +
+ {/* Tool content */} +
+ +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/tools/ToolPicker.tsx b/frontend/src/components/tools/ToolPicker.tsx index 7b678de98f..d392f21b65 100644 --- a/frontend/src/components/tools/ToolPicker.tsx +++ b/frontend/src/components/tools/ToolPicker.tsx @@ -1,32 +1,21 @@ -import React, { useState } from "react"; -import { Box, Text, Stack, Button, TextInput, Group } from "@mantine/core"; +import React from "react"; +import { Box, Text, Stack, Button } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { ToolRegistry } from "../../types/tool"; interface ToolPickerProps { selectedToolKey: string | null; onSelect: (id: string) => void; - toolRegistry: ToolRegistry; + /** Pre-filtered tools to display */ + filteredTools: [string, ToolRegistry[string]][]; } -const ToolPicker = ({ selectedToolKey, onSelect, toolRegistry }: ToolPickerProps) => { +const ToolPicker = ({ selectedToolKey, onSelect, filteredTools }: ToolPickerProps) => { const { t } = useTranslation(); - const [search, setSearch] = useState(""); - - const filteredTools = Object.entries(toolRegistry).filter(([_, { name }]) => - name.toLowerCase().includes(search.toLowerCase()) - ); return ( - - setSearch(e.currentTarget.value)} - mb="md" - autoComplete="off" - /> - + + {filteredTools.length === 0 ? ( {t("toolPicker.noToolsFound", "No tools found")} diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx new file mode 100644 index 0000000000..47f42b0118 --- /dev/null +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -0,0 +1,221 @@ +/** + * ToolWorkflowContext - Manages tool selection, UI state, and workflow coordination + * Eliminates prop drilling with a single, simple context + */ + +import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react'; +import { useToolManagement } from '../hooks/useToolManagement'; +import { ToolConfiguration } from '../types/tool'; +import { PageEditorFunctions } from '../types/pageEditor'; + +// State interface +interface ToolWorkflowState { + // UI State + sidebarsVisible: boolean; + leftPanelView: 'toolPicker' | 'toolContent'; + readerMode: boolean; + + // File/Preview State + previewFile: File | null; + pageEditorFunctions: PageEditorFunctions | null; + + // Search State + searchQuery: string; +} + +// Actions +type ToolWorkflowAction = + | { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean } + | { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' } + | { type: 'SET_READER_MODE'; payload: boolean } + | { type: 'SET_PREVIEW_FILE'; payload: File | null } + | { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null } + | { type: 'SET_SEARCH_QUERY'; payload: string } + | { type: 'RESET_UI_STATE' }; + +// Initial state +const initialState: ToolWorkflowState = { + sidebarsVisible: true, + leftPanelView: 'toolPicker', + readerMode: false, + previewFile: null, + pageEditorFunctions: null, + searchQuery: '', +}; + +// Reducer +function toolWorkflowReducer(state: ToolWorkflowState, action: ToolWorkflowAction): ToolWorkflowState { + switch (action.type) { + case 'SET_SIDEBARS_VISIBLE': + return { ...state, sidebarsVisible: action.payload }; + case 'SET_LEFT_PANEL_VIEW': + return { ...state, leftPanelView: action.payload }; + case 'SET_READER_MODE': + return { ...state, readerMode: action.payload }; + case 'SET_PREVIEW_FILE': + return { ...state, previewFile: action.payload }; + case 'SET_PAGE_EDITOR_FUNCTIONS': + return { ...state, pageEditorFunctions: action.payload }; + case 'SET_SEARCH_QUERY': + return { ...state, searchQuery: action.payload }; + case 'RESET_UI_STATE': + return { ...initialState, searchQuery: state.searchQuery }; // Preserve search + default: + return state; + } +} + +// Context value interface +interface ToolWorkflowContextValue extends ToolWorkflowState { + // Tool management (from hook) + selectedToolKey: string | null; + selectedTool: ToolConfiguration | null; + toolRegistry: any; // From useToolManagement + + // UI Actions + setSidebarsVisible: (visible: boolean) => void; + setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void; + setReaderMode: (mode: boolean) => void; + setPreviewFile: (file: File | null) => void; + setPageEditorFunctions: (functions: PageEditorFunctions | null) => void; + setSearchQuery: (query: string) => void; + + // Tool Actions + selectTool: (toolId: string) => void; + clearToolSelection: () => void; + + // Workflow Actions (compound actions) + handleToolSelect: (toolId: string) => void; + handleBackToTools: () => void; + handleReaderToggle: () => void; + + // Computed values + filteredTools: [string, any][]; // Filtered by search + isPanelVisible: boolean; +} + +const ToolWorkflowContext = createContext(undefined); + +// Provider component +interface ToolWorkflowProviderProps { + children: React.ReactNode; + /** Handler for view changes (passed from parent) */ + onViewChange?: (view: string) => void; +} + +export function ToolWorkflowProvider({ children, onViewChange }: ToolWorkflowProviderProps) { + const [state, dispatch] = useReducer(toolWorkflowReducer, initialState); + + // Tool management hook + const { + selectedToolKey, + selectedTool, + toolRegistry, + selectTool, + clearToolSelection, + } = useToolManagement(); + + // UI Action creators + const setSidebarsVisible = useCallback((visible: boolean) => { + dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible }); + }, []); + + const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => { + dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view }); + }, []); + + const setReaderMode = useCallback((mode: boolean) => { + dispatch({ type: 'SET_READER_MODE', payload: mode }); + }, []); + + const setPreviewFile = useCallback((file: File | null) => { + dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); + }, []); + + const setPageEditorFunctions = useCallback((functions: PageEditorFunctions | null) => { + dispatch({ type: 'SET_PAGE_EDITOR_FUNCTIONS', payload: functions }); + }, []); + + const setSearchQuery = useCallback((query: string) => { + dispatch({ type: 'SET_SEARCH_QUERY', payload: query }); + }, []); + + // Workflow actions (compound actions that coordinate multiple state changes) + const handleToolSelect = useCallback((toolId: string) => { + selectTool(toolId); + onViewChange?.('fileEditor'); + setLeftPanelView('toolContent'); + setReaderMode(false); + }, [selectTool, onViewChange, setLeftPanelView, setReaderMode]); + + const handleBackToTools = useCallback(() => { + setLeftPanelView('toolPicker'); + setReaderMode(false); + clearToolSelection(); + }, [setLeftPanelView, setReaderMode, clearToolSelection]); + + const handleReaderToggle = useCallback(() => { + setReaderMode(true); + }, [setReaderMode]); + + // Filter tools based on search query + const filteredTools = useMemo(() => { + if (!toolRegistry) return []; + return Object.entries(toolRegistry).filter(([_, { name }]) => + name.toLowerCase().includes(state.searchQuery.toLowerCase()) + ); + }, [toolRegistry, state.searchQuery]); + + const isPanelVisible = useMemo(() => + state.sidebarsVisible && !state.readerMode, + [state.sidebarsVisible, state.readerMode] + ); + + // Simple context value with basic memoization + const contextValue = useMemo((): ToolWorkflowContextValue => ({ + // State + ...state, + selectedToolKey, + selectedTool, + toolRegistry, + + // Actions + setSidebarsVisible, + setLeftPanelView, + setReaderMode, + setPreviewFile, + setPageEditorFunctions, + setSearchQuery, + selectTool, + clearToolSelection, + + // Workflow Actions + handleToolSelect, + handleBackToTools, + handleReaderToggle, + + // Computed + filteredTools, + isPanelVisible, + }), [state, selectedToolKey, selectedTool, toolRegistry, filteredTools, isPanelVisible]); + + return ( + + {children} + + ); +} + +// Custom hook to use the context +export function useToolWorkflow(): ToolWorkflowContextValue { + const context = useContext(ToolWorkflowContext); + if (!context) { + throw new Error('useToolWorkflow must be used within a ToolWorkflowProvider'); + } + return context; +} + +// Convenience exports for specific use cases (optional - components can use useToolWorkflow directly) +export const useToolSelection = useToolWorkflow; +export const useToolPanelState = useToolWorkflow; +export const useWorkbenchState = useToolWorkflow; \ No newline at end of file diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index b7a352f0f5..e9009282e1 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,57 +1,26 @@ -import React, { useState, useCallback, useEffect, useRef } from "react"; -import { useTranslation } from 'react-i18next'; +import React, { useEffect } from "react"; import { useFileContext } from "../contexts/FileContext"; import { FileSelectionProvider, useFileSelection } from "../contexts/FileSelectionContext"; +import { ToolWorkflowProvider, useToolSelection } from "../contexts/ToolWorkflowContext"; +import { Group } from "@mantine/core"; import { SidebarProvider, useSidebarContext } from "../contexts/SidebarContext"; -import { useToolManagement } from "../hooks/useToolManagement"; -import { useFileHandler } from "../hooks/useFileHandler"; -import { Group, Box, Button } from "@mantine/core"; -import { useRainbowThemeContext } from "../components/shared/RainbowThemeProvider"; -import { PageEditorFunctions } from "../types/pageEditor"; -import { SidebarRefs, SidebarState } from "../types/sidebar"; -import rainbowStyles from '../styles/rainbow.module.css'; -import ToolPicker from "../components/tools/ToolPicker"; -import TopControls from "../components/shared/TopControls"; -import FileEditor from "../components/fileEditor/FileEditor"; -import PageEditor from "../components/pageEditor/PageEditor"; -import PageEditorControls from "../components/pageEditor/PageEditorControls"; -import Viewer from "../components/viewer/Viewer"; -import ToolRenderer from "../components/tools/ToolRenderer"; +import ToolPanel from "../components/tools/ToolPanel"; +import Workbench from "../components/layout/Workbench"; import QuickAccessBar from "../components/shared/QuickAccessBar"; -import LandingPage from "../components/shared/LandingPage"; import FileManager from "../components/FileManager"; function HomePageContent() { - const { t } = useTranslation(); - const { isRainbowMode } = useRainbowThemeContext(); const { - sidebarState, sidebarRefs, - setSidebarsVisible, - setLeftPanelView, - setReaderMode } = useSidebarContext(); - const { sidebarsVisible, leftPanelView, readerMode } = sidebarState; - const { quickAccessRef, toolPanelRef } = sidebarRefs; + const { quickAccessRef } = sidebarRefs; - const fileContext = useFileContext(); - const { activeFiles, currentView, setCurrentView } = fileContext; const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection(); - const { addToActiveFiles } = useFileHandler(); - const { - selectedToolKey, - selectedTool, - toolRegistry, - selectTool, - clearToolSelection, - } = useToolManagement(); - - const [pageEditorFunctions, setPageEditorFunctions] = useState(null); - const [previewFile, setPreviewFile] = useState(null); + const { selectedTool } = useToolSelection(); // Update file selection context when tool changes useEffect(() => { @@ -65,232 +34,30 @@ function HomePageContent() { } }, [selectedTool, setMaxFiles, setIsToolMode, setSelectedFiles]); - - - const handleToolSelect = useCallback( - (id: string) => { - selectTool(id); - setCurrentView('fileEditor'); // Tools use fileEditor view for file selection - setLeftPanelView('toolContent'); - setReaderMode(false); - }, - [selectTool, setCurrentView] - ); - - const handleQuickAccessTools = useCallback(() => { - setLeftPanelView('toolPicker'); - setReaderMode(false); - clearToolSelection(); - }, [clearToolSelection]); - - const handleReaderToggle = useCallback(() => { - setReaderMode(true); - }, [readerMode]); - - const handleViewChange = useCallback((view: string) => { - setCurrentView(view as any); - }, [setCurrentView]); - - - - return ( - {/* Quick Access Bar */} - - {/* Left: Tool Picker or Selected Tool Panel */} -
-
- {leftPanelView === 'toolPicker' ? ( - // Tool Picker View -
- -
- ) : ( - // Selected Tool Content View -
- {/* Back button */} -
- -
- - {/* Tool title */} -
-

{selectedTool?.name}

-
- - {/* Tool content */} -
- -
-
- )} -
-
- - {/* Main View */} - - {/* Top Controls */} - - {/* Main content area */} - - {!activeFiles[0] ? ( - - ) : currentView === "fileEditor" ? ( - { - handleViewChange("pageEditor"); - }, - onMergeFiles: (filesToMerge) => { - filesToMerge.forEach(addToActiveFiles); - handleViewChange("viewer"); - } - })} - /> - ) : currentView === "viewer" ? ( - { - setPreviewFile(null); // Clear preview file - const previousMode = sessionStorage.getItem('previousMode'); - if (previousMode === 'split') { - selectTool('split'); - setCurrentView('split'); - setLeftPanelView('toolContent'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'compress') { - selectTool('compress'); - setCurrentView('compress'); - setLeftPanelView('toolContent'); - sessionStorage.removeItem('previousMode'); - } else if (previousMode === 'convert') { - selectTool('convert'); - setCurrentView('convert'); - setLeftPanelView('toolContent'); - sessionStorage.removeItem('previousMode'); - } else { - setCurrentView('fileEditor'); - } - } - })} - /> - ) : currentView === "pageEditor" ? ( - <> - - {pageEditorFunctions && ( - - )} - - ) : selectedToolKey && selectedTool ? ( - // Fallback: if tool is selected but not in fileEditor view, show tool in main area - - ) : ( - - )} - - - - {/* Global Modals */} + ref={quickAccessRef} /> + +
); } -// Main HomePage component wrapped with FileSelectionProvider export default function HomePage() { + const { setCurrentView } = useFileContext(); return ( - - - + + + + + ); } diff --git a/frontend/src/types/sidebar.ts b/frontend/src/types/sidebar.ts index 60dcd029d0..b286f0b826 100644 --- a/frontend/src/types/sidebar.ts +++ b/frontend/src/types/sidebar.ts @@ -28,12 +28,6 @@ export interface SidebarProviderProps { children: React.ReactNode; } -// QuickAccessBar related interfaces -export interface QuickAccessBarProps { - onToolsClick: () => void; - onReaderToggle: () => void; -} - export interface ButtonConfig { id: string; name: string; From 507ad1dc6197a3887c3ec9c051afc8daaf288050 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:01:56 +0100 Subject: [PATCH 62/77] Feature/v2/shared tool hooks (#4134) # 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: Reece Browne --- CLAUDE.md | 80 ++- .../components/tools/split/SplitSettings.tsx | 17 +- .../tools/compress/useCompressOperation.ts | 287 ++--------- .../tools/convert/useConvertOperation.ts | 464 ++++-------------- .../src/hooks/tools/ocr/useOCROperation.ts | 443 ++++------------- .../src/hooks/tools/shared/useToolApiCalls.ts | 86 ++++ .../hooks/tools/shared/useToolOperation.ts | 264 ++++++++++ .../hooks/tools/shared/useToolResources.ts | 114 +++++ .../src/hooks/tools/shared/useToolState.ts | 137 ++++++ .../hooks/tools/split/useSplitOperation.ts | 302 +++--------- .../hooks/tools/split/useSplitParameters.ts | 15 +- .../services/enhancedPDFProcessingService.ts | 5 +- .../tests/convert/ConvertIntegration.test.tsx | 2 +- frontend/src/tools/Split.tsx | 7 +- frontend/src/types/processing.ts | 1 - frontend/src/utils/toolErrorHandler.ts | 33 ++ frontend/src/utils/toolOperationTracker.ts | 28 ++ frontend/src/utils/toolResponseProcessor.ts | 25 + 18 files changed, 1068 insertions(+), 1242 deletions(-) create mode 100644 frontend/src/hooks/tools/shared/useToolApiCalls.ts create mode 100644 frontend/src/hooks/tools/shared/useToolOperation.ts create mode 100644 frontend/src/hooks/tools/shared/useToolResources.ts create mode 100644 frontend/src/hooks/tools/shared/useToolState.ts create mode 100644 frontend/src/utils/toolErrorHandler.ts create mode 100644 frontend/src/utils/toolOperationTracker.ts create mode 100644 frontend/src/utils/toolResponseProcessor.ts diff --git a/CLAUDE.md b/CLAUDE.md index 8bdd7c235f..be4e922012 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,12 +59,73 @@ Frontend designed for **stateful document processing**: Without cleanup: browser crashes with memory leaks. #### Tool Development -- **Pattern**: Follow `src/tools/Split.tsx` as reference implementation -- **File Access**: Tools receive `selectedFiles` prop (computed from activeFiles based on user selection) -- **File Selection**: Users select files in FileEditor (tool mode) → stored as IDs → computed to File objects for tools -- **Integration**: All files are part of FileContext ecosystem - automatic memory management and operation tracking -- **Parameters**: Tool parameter handling patterns still being standardized -- **Preview Integration**: Tools can implement preview functionality (see Split tool's thumbnail preview) + +**Architecture**: Modular hook-based system with clear separation of concerns: + +- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook + - Coordinates all tool operations with consistent interface + - Integrates with FileContext for operation tracking + - Handles validation, error handling, and UI state management + +- **Supporting Hooks**: + - **useToolState**: UI state management (loading, progress, error, files) + - **useToolApiCalls**: HTTP requests and file processing + - **useToolResources**: Blob URLs, thumbnails, ZIP downloads + +- **Utilities**: + - **toolErrorHandler**: Standardized error extraction and i18n support + - **toolResponseProcessor**: API response handling (single/zip/custom) + - **toolOperationTracker**: FileContext integration utilities + +**Three Tool Patterns**: + +**Pattern 1: Single-File Tools** (Individual processing) +- Backend processes one file per API call +- Set `multiFileEndpoint: false` +- Examples: Compress, Rotate +```typescript +return useToolOperation({ + operationType: 'compress', + endpoint: '/api/v1/misc/compress-pdf', + buildFormData: (params, file: File) => { /* single file */ }, + multiFileEndpoint: false, + filePrefix: 'compressed_' +}); +``` + +**Pattern 2: Multi-File Tools** (Batch processing) +- Backend accepts `MultipartFile[]` arrays in single API call +- Set `multiFileEndpoint: true` +- Examples: Split, Merge, Overlay +```typescript +return useToolOperation({ + operationType: 'split', + endpoint: '/api/v1/general/split-pages', + buildFormData: (params, files: File[]) => { /* all files */ }, + multiFileEndpoint: true, + filePrefix: 'split_' +}); +``` + +**Pattern 3: Complex Tools** (Custom processing) +- Tools with complex routing logic or non-standard processing +- Provide `customProcessor` for full control +- Examples: Convert, OCR +```typescript +return useToolOperation({ + operationType: 'convert', + customProcessor: async (params, files) => { /* custom logic */ }, + filePrefix: 'converted_' +}); +``` + +**Benefits**: +- **No Timeouts**: Operations run until completion (supports 100GB+ files) +- **Consistent**: All tools follow same pattern and interface +- **Maintainable**: Single responsibility hooks, easy to test and modify +- **i18n Ready**: Built-in internationalization support +- **Type Safe**: Full TypeScript support with generic interfaces +- **Memory Safe**: Automatic resource cleanup and blob URL management ## Architecture Overview @@ -126,7 +187,10 @@ Without cleanup: browser crashes with memory leaks. - **Core Status**: React SPA architecture complete with multi-tool workflow support - **State Management**: FileContext handles all file operations and tool navigation - **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+) -- **Tool Integration**: Standardized tool interface - see `src/tools/Split.tsx` as reference +- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator + - Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources` + - Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker` + - Pattern: Each tool creates focused operation hook, UI consumes state/actions - **Preview System**: Tool results can be previewed without polluting file context (Split tool example) - **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing @@ -141,7 +205,7 @@ Without cleanup: browser crashes with memory leaks. - **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation - **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling - **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code -- **Tool Development**: New tools should follow Split tool pattern (`src/tools/Split.tsx`) +- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`) - **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes - **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation) diff --git a/frontend/src/components/tools/split/SplitSettings.tsx b/frontend/src/components/tools/split/SplitSettings.tsx index 50ca49f20e..95e972eaf3 100644 --- a/frontend/src/components/tools/split/SplitSettings.tsx +++ b/frontend/src/components/tools/split/SplitSettings.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants'; export interface SplitParameters { + mode: SplitMode | ''; pages: string; hDiv: string; vDiv: string; @@ -15,16 +16,12 @@ export interface SplitParameters { } export interface SplitSettingsProps { - mode: SplitMode | ''; - onModeChange: (mode: SplitMode | '') => void; parameters: SplitParameters; onParameterChange: (parameter: keyof SplitParameters, value: string | boolean) => void; disabled?: boolean; } const SplitSettings = ({ - mode, - onModeChange, parameters, onParameterChange, disabled = false @@ -125,8 +122,8 @@ const SplitSettings = ({ - + - + diff --git a/frontend/src/components/tools/convert/ConvertToImageSettings.tsx b/frontend/src/components/tools/convert/ConvertToImageSettings.tsx index 0435416726..9d67bfbf68 100644 --- a/frontend/src/components/tools/convert/ConvertToImageSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToImageSettings.tsx @@ -31,7 +31,7 @@ const ConvertToImageSettings = ({ })} data={[ { value: COLOR_TYPES.COLOR, label: t("convert.color", "Color") }, - { value: COLOR_TYPES.GREYSCALE, label: t("convert.greyscale", "Greyscale") }, + { value: COLOR_TYPES.GRAYSCALE, label: t("convert.grayscale", "Grayscale") }, { value: COLOR_TYPES.BLACK_WHITE, label: t("convert.blackwhite", "Black & White") }, ]} disabled={disabled} @@ -68,4 +68,4 @@ const ConvertToImageSettings = ({ ); }; -export default ConvertToImageSettings; \ No newline at end of file +export default ConvertToImageSettings; diff --git a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx index 0422ee780c..e1a662bd27 100644 --- a/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx +++ b/frontend/src/components/tools/convert/ConvertToPdfaSettings.tsx @@ -30,7 +30,7 @@ const ConvertToPdfaSettings = ({ {t("convert.pdfaOptions", "PDF/A Options")}: {hasDigitalSignatures && ( - + {t("convert.pdfaDigitalSignatureWarning", "The PDF contains a digital signature. This will be removed in the next step.")} diff --git a/frontend/src/components/tools/split/SplitSettings.tsx b/frontend/src/components/tools/split/SplitSettings.tsx index 95e972eaf3..6320002276 100644 --- a/frontend/src/components/tools/split/SplitSettings.tsx +++ b/frontend/src/components/tools/split/SplitSettings.tsx @@ -1,6 +1,6 @@ import { Stack, TextInput, Select, Checkbox } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import { SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants'; +import { isSplitMode, SPLIT_MODES, SPLIT_TYPES, type SplitMode, type SplitType } from '../../../constants/splitConstants'; export interface SplitParameters { mode: SplitMode | ''; @@ -123,7 +123,7 @@ const SplitSettings = ({ label="Choose split method" placeholder="Select how to split the PDF" value={parameters.mode} - onChange={(v) => v && onParameterChange('mode', v)} + onChange={(v) => isSplitMode(v) && onParameterChange('mode', v)} disabled={disabled} data={[ { value: SPLIT_MODES.BY_PAGES, label: t("split.header", "Split by Pages") + " (e.g. 1,3,5-10)" }, diff --git a/frontend/src/components/viewer/Viewer.tsx b/frontend/src/components/viewer/Viewer.tsx index 065ce5824c..6e08962b10 100644 --- a/frontend/src/components/viewer/Viewer.tsx +++ b/frontend/src/components/viewer/Viewer.tsx @@ -137,7 +137,7 @@ export interface ViewerProps { sidebarsVisible: boolean; setSidebarsVisible: (v: boolean) => void; onClose?: () => void; - previewFile?: File; // For preview mode - bypasses context + previewFile: File | null; // For preview mode - bypasses context } const Viewer = ({ @@ -148,18 +148,13 @@ const Viewer = ({ }: ViewerProps) => { const { t } = useTranslation(); const theme = useMantineTheme(); - + // Get current file from FileContext const { getCurrentFile, getCurrentProcessedFile, clearAllFiles, addFiles, activeFiles } = useFileContext(); - const currentFile = getCurrentFile(); - const processedFile = getCurrentProcessedFile(); - - // Convert File to FileWithUrl format for viewer - const pdfFile = useFileWithUrl(currentFile); - + // Tab management for multiple files const [activeTab, setActiveTab] = useState("0"); - + // Reset PDF state when switching tabs const handleTabChange = (newTab: string) => { setActiveTab(newTab); @@ -183,7 +178,7 @@ const Viewer = ({ const file2WithUrl = useFileWithUrl(activeFiles[2]); const file3WithUrl = useFileWithUrl(activeFiles[3]); const file4WithUrl = useFileWithUrl(activeFiles[4]); - + const filesWithUrls = React.useMemo(() => { return [file0WithUrl, file1WithUrl, file2WithUrl, file3WithUrl, file4WithUrl] .slice(0, activeFiles.length) @@ -197,11 +192,11 @@ const Viewer = ({ if (!(previewFile instanceof File)) { return null; } - + if (previewFile.size === 0) { return null; } - + return { file: previewFile, url: null }; } else { // Use the file from the active tab @@ -262,12 +257,12 @@ const Viewer = ({ // Progressive preloading function const startProgressivePreload = async () => { if (!pdfDocRef.current || preloadingRef.current || numPages === 0) return; - + preloadingRef.current = true; - + // Start with first few pages for immediate viewing const priorityPages = [0, 1, 2, 3, 4]; // First 5 pages - + // Render priority pages first for (const pageIndex of priorityPages) { if (pageIndex < numPages && !pageImages[pageIndex]) { @@ -276,7 +271,7 @@ const Viewer = ({ await new Promise(resolve => setTimeout(resolve, 50)); } } - + // Then render remaining pages in background for (let pageIndex = 5; pageIndex < numPages; pageIndex++) { if (!pageImages[pageIndex]) { @@ -285,7 +280,7 @@ const Viewer = ({ await new Promise(resolve => setTimeout(resolve, 100)); } } - + preloadingRef.current = false; }; @@ -300,15 +295,15 @@ const Viewer = ({ const scrollToPage = (pageNumber: number) => { const el = pageRefs.current[pageNumber - 1]; const scrollArea = scrollAreaRef.current; - + if (el && scrollArea) { const scrollAreaRect = scrollArea.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); const currentScrollTop = scrollArea.scrollTop; - + // Position page near top of viewport with some padding const targetScrollTop = currentScrollTop + (elRect.top - scrollAreaRect.top) - 20; - + scrollArea.scrollTo({ top: targetScrollTop, behavior: "smooth" @@ -364,7 +359,7 @@ const Viewer = ({ setLoading(true); try { let pdfData; - + // For preview files, use ArrayBuffer directly to avoid blob URL issues if (previewFile && effectiveFile.file === previewFile) { const arrayBuffer = await previewFile.arrayBuffer(); @@ -446,7 +441,7 @@ const Viewer = ({ )} - + {!effectiveFile ? (
Error: No file provided to viewer @@ -455,8 +450,8 @@ const Viewer = ({ <> {/* Tabs for multiple files */} {activeFiles.length > 1 && !previewFile && ( - )} - + {loading ? (
diff --git a/frontend/src/constants/convertConstants.ts b/frontend/src/constants/convertConstants.ts index f371c70815..5978d523b1 100644 --- a/frontend/src/constants/convertConstants.ts +++ b/frontend/src/constants/convertConstants.ts @@ -1,7 +1,7 @@ export const COLOR_TYPES = { COLOR: 'color', - GREYSCALE: 'greyscale', + GRAYSCALE: 'grayscale', BLACK_WHITE: 'blackwhite' } as const; @@ -135,7 +135,7 @@ export const EXTENSION_TO_ENDPOINT: Record> = { 'docx': { 'pdf': 'file-to-pdf' }, 'doc': { 'pdf': 'file-to-pdf' }, 'odt': { 'pdf': 'file-to-pdf' }, 'xlsx': { 'pdf': 'file-to-pdf' }, 'xls': { 'pdf': 'file-to-pdf' }, 'ods': { 'pdf': 'file-to-pdf' }, 'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' }, - 'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' }, + 'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' }, 'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' }, 'svg': { 'pdf': 'img-to-pdf' }, 'html': { 'pdf': 'html-to-pdf' }, 'zip': { 'pdf': 'html-to-pdf' }, @@ -146,4 +146,4 @@ export const EXTENSION_TO_ENDPOINT: Record> = { export type ColorType = typeof COLOR_TYPES[keyof typeof COLOR_TYPES]; export type OutputOption = typeof OUTPUT_OPTIONS[keyof typeof OUTPUT_OPTIONS]; -export type FitOption = typeof FIT_OPTIONS[keyof typeof FIT_OPTIONS]; \ No newline at end of file +export type FitOption = typeof FIT_OPTIONS[keyof typeof FIT_OPTIONS]; diff --git a/frontend/src/constants/splitConstants.ts b/frontend/src/constants/splitConstants.ts index 0da43ac57c..1e70986691 100644 --- a/frontend/src/constants/splitConstants.ts +++ b/frontend/src/constants/splitConstants.ts @@ -1,6 +1,6 @@ export const SPLIT_MODES = { BY_PAGES: 'byPages', - BY_SECTIONS: 'bySections', + BY_SECTIONS: 'bySections', BY_SIZE_OR_COUNT: 'bySizeOrCount', BY_CHAPTERS: 'byChapters' } as const; @@ -19,4 +19,12 @@ export const ENDPOINTS = { } as const; export type SplitMode = typeof SPLIT_MODES[keyof typeof SPLIT_MODES]; -export type SplitType = typeof SPLIT_TYPES[keyof typeof SPLIT_TYPES]; \ No newline at end of file +export type SplitType = typeof SPLIT_TYPES[keyof typeof SPLIT_TYPES]; + +export const isSplitMode = (value: string | null): value is SplitMode => { + return Object.values(SPLIT_MODES).includes(value as SplitMode); +} + +export const isSplitType = (value: string | null): value is SplitType => { + return Object.values(SPLIT_TYPES).includes(value as SplitType); +} diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index f84d2ec8bd..c49a43e389 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -3,9 +3,9 @@ */ import React, { createContext, useContext, useReducer, useCallback, useEffect, useRef } from 'react'; -import { - FileContextValue, - FileContextState, +import { + FileContextValue, + FileContextState, FileContextProviderProps, ModeType, ViewType, @@ -53,7 +53,7 @@ const initialState: FileContextState = { }; // Action types -type FileContextAction = +type FileContextAction = | { type: 'SET_ACTIVE_FILES'; payload: File[] } | { type: 'ADD_FILES'; payload: File[] } | { type: 'REMOVE_FILES'; payload: string[] } @@ -126,7 +126,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): case 'SET_CURRENT_MODE': const coreViews = ['viewer', 'pageEditor', 'fileEditor']; const isToolMode = !coreViews.includes(action.payload); - + return { ...state, currentMode: action.payload, @@ -193,8 +193,8 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): const existing = newHistory.get(action.payload.fileId); newHistory.set(action.payload.fileId, { fileId: action.payload.fileId, - pageOperations: existing ? - [...existing.pageOperations, ...action.payload.operations] : + pageOperations: existing ? + [...existing.pageOperations, ...action.payload.operations] : action.payload.operations, lastModified: Date.now() }); @@ -213,7 +213,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): const { fileId, operation } = action.payload; const newOperationHistory = new Map(state.fileOperationHistory); const existingHistory = newOperationHistory.get(fileId); - + if (existingHistory) { // Add operation to existing history newOperationHistory.set(fileId, { @@ -231,7 +231,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): lastModified: Date.now() }); } - + return { ...state, fileOperationHistory: newOperationHistory @@ -240,10 +240,10 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): case 'MARK_OPERATION_APPLIED': const appliedHistory = new Map(state.fileOperationHistory); const appliedFileHistory = appliedHistory.get(action.payload.fileId); - + if (appliedFileHistory) { - const updatedOperations = appliedFileHistory.operations.map(op => - op.id === action.payload.operationId + const updatedOperations = appliedFileHistory.operations.map(op => + op.id === action.payload.operationId ? { ...op, status: 'applied' as const } : op ); @@ -253,7 +253,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): lastModified: Date.now() }); } - + return { ...state, fileOperationHistory: appliedHistory @@ -262,12 +262,12 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): case 'MARK_OPERATION_FAILED': const failedHistory = new Map(state.fileOperationHistory); const failedFileHistory = failedHistory.get(action.payload.fileId); - + if (failedFileHistory) { - const updatedOperations = failedFileHistory.operations.map(op => - op.id === action.payload.operationId - ? { - ...op, + const updatedOperations = failedFileHistory.operations.map(op => + op.id === action.payload.operationId + ? { + ...op, status: 'failed' as const, metadata: { ...op.metadata, error: action.payload.error } } @@ -279,7 +279,7 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): lastModified: Date.now() }); } - + return { ...state, fileOperationHistory: failedHistory @@ -337,19 +337,19 @@ function fileContextReducer(state: FileContextState, action: FileContextAction): const FileContext = createContext(undefined); // Provider component -export function FileContextProvider({ - children, +export function FileContextProvider({ + children, enableUrlSync = true, enablePersistence = true, maxCacheSize = 1024 * 1024 * 1024 // 1GB }: FileContextProviderProps) { const [state, dispatch] = useReducer(fileContextReducer, initialState); - + // Cleanup timers and refs const cleanupTimers = useRef>(new Map()); const blobUrls = useRef>(new Set()); const pdfDocuments = useRef>(new Map()); - + // Enhanced file processing hook const { processedFiles, @@ -367,11 +367,11 @@ export function FileContextProvider({ // Update processed files when they change useEffect(() => { dispatch({ type: 'SET_PROCESSED_FILES', payload: processedFiles }); - dispatch({ - type: 'SET_PROCESSING', - payload: { - isProcessing: globalProcessing, - progress: processingProgress.overall + dispatch({ + type: 'SET_PROCESSING', + payload: { + isProcessing: globalProcessing, + progress: processingProgress.overall } }); }, [processedFiles, globalProcessing, processingProgress.overall]); @@ -397,7 +397,7 @@ export function FileContextProvider({ const cleanupFile = useCallback(async (fileId: string) => { console.log('Cleaning up file:', fileId); - + try { // Cancel any pending cleanup timer const timer = cleanupTimers.current.get(fileId); @@ -425,7 +425,7 @@ export function FileContextProvider({ const cleanupAllFiles = useCallback(() => { console.log('Cleaning up all files'); - + try { // Clear all timers cleanupTimers.current.forEach(timer => clearTimeout(timer)); @@ -461,7 +461,8 @@ export function FileContextProvider({ // Force garbage collection hint if (typeof window !== 'undefined' && window.gc) { - setTimeout(() => window.gc(), 100); + let gc = window.gc + setTimeout(() => gc(), 100); } } catch (error) { @@ -486,14 +487,14 @@ export function FileContextProvider({ const timer = setTimeout(() => { cleanupFile(fileId); }, delay); - + cleanupTimers.current.set(fileId, timer); }, [cleanupFile]); // Action implementations const addFiles = useCallback(async (files: File[]): Promise => { dispatch({ type: 'ADD_FILES', payload: files }); - + // Auto-save to IndexedDB if persistence enabled if (enablePersistence) { for (const file of files) { @@ -504,7 +505,7 @@ export function FileContextProvider({ // File doesn't have explicit ID, store it with thumbnail try { // Generate thumbnail for better recent files experience - const thumbnail = await thumbnailGenerationService.generateThumbnail(file); + const thumbnail = await (thumbnailGenerationService as any /* FIX ME */).generateThumbnail(file); const storedFile = await fileStorage.storeFile(file, thumbnail); // Add the ID to the file object Object.defineProperty(file, 'id', { value: storedFile.id, writable: false }); @@ -520,7 +521,7 @@ export function FileContextProvider({ } } } - + // Return files with their IDs assigned return files; }, [enablePersistence]); @@ -532,9 +533,9 @@ export function FileContextProvider({ enhancedPDFProcessingService.cancelProcessing(fileId); cleanupFile(fileId); }); - + dispatch({ type: 'REMOVE_FILES', payload: fileIds }); - + // Remove from IndexedDB only if requested if (enablePersistence && deleteFromStorage) { fileIds.forEach(async (fileId) => { @@ -557,7 +558,7 @@ export function FileContextProvider({ const clearAllFiles = useCallback(() => { // Cleanup all memory before clearing files cleanupAllFiles(); - + dispatch({ type: 'SET_ACTIVE_FILES', payload: [] }); dispatch({ type: 'CLEAR_SELECTIONS' }); }, [cleanupAllFiles]); @@ -594,11 +595,12 @@ export function FileContextProvider({ const setCurrentMode = useCallback((mode: ModeType) => { requestNavigation(() => { dispatch({ type: 'SET_CURRENT_MODE', payload: mode }); - + if (state.currentMode !== mode && state.activeFiles.length > 0) { if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) { + let gc = window.gc; window.requestIdleCallback(() => { - window.gc(); + gc(); }, { timeout: 5000 }); } } @@ -608,11 +610,12 @@ export function FileContextProvider({ const setCurrentView = useCallback((view: ViewType) => { requestNavigation(() => { dispatch({ type: 'SET_CURRENT_VIEW', payload: view }); - + if (state.currentView !== view && state.activeFiles.length > 0) { if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) { + let gc = window.gc; window.requestIdleCallback(() => { - window.gc(); + gc(); }, { timeout: 5000 }); } } @@ -642,8 +645,8 @@ export function FileContextProvider({ }, []); const applyPageOperations = useCallback((fileId: string, operations: PageOperation[]) => { - dispatch({ - type: 'ADD_PAGE_OPERATIONS', + dispatch({ + type: 'ADD_PAGE_OPERATIONS', payload: { fileId, operations } }); }, []); @@ -718,18 +721,18 @@ export function FileContextProvider({ // Context persistence const saveContext = useCallback(async () => { if (!enablePersistence) return; - + try { const contextData = { currentView: state.currentView, currentTool: state.currentTool, selectedFileIds: state.selectedFileIds, - selectedPageIds: state.selectedPageIds, + selectedPageNumbers: state.selectedPageNumbers, viewerConfig: state.viewerConfig, lastExportConfig: state.lastExportConfig, timestamp: Date.now() }; - + localStorage.setItem('fileContext', JSON.stringify(contextData)); } catch (error) { console.error('Failed to save context:', error); @@ -738,7 +741,7 @@ export function FileContextProvider({ const loadContext = useCallback(async () => { if (!enablePersistence) return; - + try { const saved = localStorage.getItem('fileContext'); if (saved) { @@ -779,7 +782,7 @@ export function FileContextProvider({ const contextValue: FileContextValue = { // State ...state, - + // Actions addFiles, removeFiles, @@ -804,7 +807,7 @@ export function FileContextProvider({ saveContext, loadContext, resetContext, - + // Operation history management recordOperation, markOperationApplied, @@ -812,13 +815,13 @@ export function FileContextProvider({ getFileHistory, getAppliedOperations, clearFileHistory, - + // Navigation guard system setHasUnsavedChanges, requestNavigation, confirmNavigation, cancelNavigation, - + // Memory management trackBlobUrl, trackPdfDocument, @@ -852,17 +855,17 @@ export function useCurrentFile() { } export function useFileSelection() { - const { - selectedFileIds, - selectedPageIds, - setSelectedFiles, - setSelectedPages, - clearSelections + const { + selectedFileIds, + selectedPageNumbers, + setSelectedFiles, + setSelectedPages, + clearSelections } = useFileContext(); - + return { selectedFileIds, - selectedPageIds, + selectedPageNumbers, setSelectedFiles, setSelectedPages, clearSelections @@ -875,4 +878,4 @@ export function useViewerState() { config: viewerConfig, updateConfig: updateViewerConfig }; -} \ No newline at end of file +} diff --git a/frontend/src/contexts/FileManagerContext.tsx b/frontend/src/contexts/FileManagerContext.tsx index c7f924e8e0..98b93cf0ba 100644 --- a/frontend/src/contexts/FileManagerContext.tsx +++ b/frontend/src/contexts/FileManagerContext.tsx @@ -10,8 +10,8 @@ interface FileManagerContextValue { searchTerm: string; selectedFiles: FileWithUrl[]; filteredFiles: FileWithUrl[]; - fileInputRef: React.RefObject; - + fileInputRef: React.RefObject; + // Handlers onSourceChange: (source: 'recent' | 'local' | 'drive') => void; onLocalFileClick: () => void; @@ -21,7 +21,7 @@ interface FileManagerContextValue { onOpenFiles: () => void; onSearchChange: (value: string) => void; onFileInputChange: (event: React.ChangeEvent) => void; - + // External props recentFiles: FileWithUrl[]; isFileSupported: (fileName: string) => boolean; @@ -61,7 +61,7 @@ export const FileManagerProvider: React.FC = ({ const [selectedFileIds, setSelectedFileIds] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const fileInputRef = useRef(null); - + // Track blob URLs for cleanup const createdBlobUrls = useRef>(new Set()); @@ -85,10 +85,14 @@ export const FileManagerProvider: React.FC = ({ const handleFileSelect = useCallback((file: FileWithUrl) => { setSelectedFileIds(prev => { - if (prev.includes(file.id)) { - return prev.filter(id => id !== file.id); + if (file.id) { + if (prev.includes(file.id)) { + return prev.filter(id => id !== file.id); + } else { + return [...prev, file.id]; + } } else { - return [...prev, file.id]; + return prev; } }); }, []); @@ -127,7 +131,7 @@ export const FileManagerProvider: React.FC = ({ const fileWithUrls = files.map(file => { const url = URL.createObjectURL(file); createdBlobUrls.current.add(url); - + return { // No ID assigned here - FileContext will handle storage and ID assignment name: file.name, @@ -137,8 +141,8 @@ export const FileManagerProvider: React.FC = ({ lastModified: file.lastModified, }; }); - - onFilesSelected(fileWithUrls); + + onFilesSelected(fileWithUrls as any /* FIX ME */); await refreshRecentFiles(); onClose(); } catch (error) { @@ -176,7 +180,7 @@ export const FileManagerProvider: React.FC = ({ selectedFiles, filteredFiles, fileInputRef, - + // Handlers onSourceChange: handleSourceChange, onLocalFileClick: handleLocalFileClick, @@ -186,7 +190,7 @@ export const FileManagerProvider: React.FC = ({ onOpenFiles: handleOpenFiles, onSearchChange: handleSearchChange, onFileInputChange: handleFileInputChange, - + // External props recentFiles, isFileSupported, @@ -203,16 +207,16 @@ export const FileManagerProvider: React.FC = ({ // Custom hook to use the context export const useFileManagerContext = (): FileManagerContextValue => { const context = useContext(FileManagerContext); - + if (!context) { throw new Error( 'useFileManagerContext must be used within a FileManagerProvider. ' + 'Make sure you wrap your component with .' ); } - + return context; }; // Export the context for advanced use cases -export { FileManagerContext }; \ No newline at end of file +export { FileManagerContext }; diff --git a/frontend/src/contexts/FilesModalContext.tsx b/frontend/src/contexts/FilesModalContext.tsx index 788db77bd0..25e6800a0b 100644 --- a/frontend/src/contexts/FilesModalContext.tsx +++ b/frontend/src/contexts/FilesModalContext.tsx @@ -7,7 +7,7 @@ interface FilesModalContextType { closeFilesModal: () => void; onFileSelect: (file: File) => void; onFilesSelect: (files: File[]) => void; - onModalClose: () => void; + onModalClose?: () => void; setOnModalClose: (callback: () => void) => void; } @@ -64,4 +64,4 @@ export const useFilesModalContext = () => { throw new Error('useFilesModalContext must be used within FilesModalProvider'); } return context; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/tools/convert/useConvertOperation.ts b/frontend/src/hooks/tools/convert/useConvertOperation.ts index 6d8d3971f4..49b55d0d1a 100644 --- a/frontend/src/hooks/tools/convert/useConvertOperation.ts +++ b/frontend/src/hooks/tools/convert/useConvertOperation.ts @@ -8,19 +8,19 @@ import { useToolOperation, ToolOperationConfig } from '../shared/useToolOperatio import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils'; const shouldProcessFilesSeparately = ( - selectedFiles: File[], + selectedFiles: File[], parameters: ConvertParameters ): boolean => { return selectedFiles.length > 1 && ( // Image to PDF with combineImages = false - ((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') && + ((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') && parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) || // PDF to image conversions (each PDF should generate its own image file) (parameters.fromExtension === 'pdf' && isImageFormat(parameters.toExtension)) || // PDF to PDF/A conversions (each PDF should be processed separately) (parameters.fromExtension === 'pdf' && parameters.toExtension === 'pdfa') || // Web files to PDF conversions (each web file should generate its own PDF) - ((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') && + ((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') && parameters.toExtension === 'pdf') || // Web files smart detection (parameters.isSmartDetection && parameters.smartDetectionType === 'web') || @@ -31,7 +31,7 @@ const shouldProcessFilesSeparately = ( const buildFormData = (parameters: ConvertParameters, selectedFiles: File[]): FormData => { const formData = new FormData(); - + selectedFiles.forEach(file => { formData.append("fileInput", file); }); @@ -77,13 +77,13 @@ const createFileFromResponse = ( ): File => { const originalName = originalFileName.split('.')[0]; const fallbackFilename = `${originalName}_converted.${targetExtension}`; - + return createFileFromApiResponse(responseData, headers, fallbackFilename); }; export const useConvertOperation = () => { const { t } = useTranslation(); - + const customConvertProcessor = useCallback(async ( parameters: ConvertParameters, selectedFiles: File[] @@ -91,7 +91,7 @@ export const useConvertOperation = () => { const processedFiles: File[] = []; const endpoint = getEndpointUrl(parameters.fromExtension, parameters.toExtension); - + if (!endpoint) { throw new Error(t('errorNotSupported', 'Unsupported conversion format')); } @@ -103,9 +103,9 @@ export const useConvertOperation = () => { try { const formData = buildFormData(parameters, [file]); const response = await axios.post(endpoint, formData, { responseType: 'blob' }); - + const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension); - + processedFiles.push(convertedFile); } catch (error) { console.warn(`Failed to convert file ${file.name}:`, error); @@ -115,11 +115,11 @@ export const useConvertOperation = () => { // Batch processing for simple cases (image→PDF combine) const formData = buildFormData(parameters, selectedFiles); const response = await axios.post(endpoint, formData, { responseType: 'blob' }); - - const baseFilename = selectedFiles.length === 1 + + const baseFilename = selectedFiles.length === 1 ? selectedFiles[0].name : 'converted_files'; - + const convertedFile = createFileFromResponse(response.data, response.headers, baseFilename, parameters.toExtension); processedFiles.push(convertedFile); @@ -131,7 +131,7 @@ export const useConvertOperation = () => { return useToolOperation({ operationType: 'convert', endpoint: '', // Not used with customProcessor but required - buildFormData, // Not used with customProcessor but required + buildFormData, // Not used with customProcessor but required filePrefix: 'converted_', customProcessor: customConvertProcessor, // Convert handles its own routing validateParams: (params) => { @@ -147,4 +147,4 @@ export const useConvertOperation = () => { return t("convert.errorConversion", "An error occurred while converting the file."); } }); -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts index 4552a4546b..32543ec91e 100644 --- a/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts +++ b/frontend/src/hooks/tools/convert/useConvertParametersAutoDetection.test.ts @@ -8,18 +8,18 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { useConvertParameters } from './useConvertParameters'; describe('useConvertParameters - Auto Detection & Smart Conversion', () => { - + describe('Single File Detection', () => { - + test('should detect single file extension and set auto-target', () => { const { result } = renderHook(() => useConvertParameters()); - + const pdfFile = [{ name: 'document.pdf' }]; - + act(() => { result.current.analyzeFileTypes(pdfFile); }); - + expect(result.current.parameters.fromExtension).toBe('pdf'); expect(result.current.parameters.toExtension).toBe(''); // No auto-selection for multiple targets expect(result.current.parameters.isSmartDetection).toBe(false); @@ -28,13 +28,13 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should handle unknown file types with file-to-pdf fallback', () => { const { result } = renderHook(() => useConvertParameters()); - + const unknownFile = [{ name: 'document.xyz' }, { name: 'image.jpggg' }]; - + act(() => { result.current.analyzeFileTypes(unknownFile); }); - + expect(result.current.parameters.fromExtension).toBe('any'); expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf expect(result.current.parameters.isSmartDetection).toBe(true); @@ -42,35 +42,35 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should handle files without extensions', () => { const { result } = renderHook(() => useConvertParameters()); - + const noExtFile = [{ name: 'document' }]; - + act(() => { result.current.analyzeFileTypes(noExtFile); }); - + expect(result.current.parameters.fromExtension).toBe('any'); expect(result.current.parameters.toExtension).toBe('pdf'); // Fallback to file-to-pdf }); - - + + }); - + describe('Multiple Identical Files', () => { - + test('should detect multiple PDF files and set auto-target', () => { const { result } = renderHook(() => useConvertParameters()); - + const pdfFiles = [ { name: 'doc1.pdf' }, { name: 'doc2.pdf' }, { name: 'doc3.pdf' } ]; - + act(() => { result.current.analyzeFileTypes(pdfFiles); }); - + expect(result.current.parameters.fromExtension).toBe('pdf'); expect(result.current.parameters.toExtension).toBe(''); // Auto-selected expect(result.current.parameters.isSmartDetection).toBe(false); @@ -79,37 +79,37 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should handle multiple unknown file types with fallback', () => { const { result } = renderHook(() => useConvertParameters()); - + const unknownFiles = [ { name: 'file1.xyz' }, { name: 'file2.xyz' } ]; - + act(() => { result.current.analyzeFileTypes(unknownFiles); }); - + expect(result.current.parameters.fromExtension).toBe('any'); expect(result.current.parameters.toExtension).toBe('pdf'); expect(result.current.parameters.isSmartDetection).toBe(false); }); }); - + describe('Smart Detection - All Images', () => { - + test('should detect all image files and enable smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const imageFiles = [ { name: 'photo1.jpg' }, { name: 'photo2.png' }, { name: 'photo3.gif' } ]; - + act(() => { result.current.analyzeFileTypes(imageFiles); }); - + expect(result.current.parameters.fromExtension).toBe('image'); expect(result.current.parameters.toExtension).toBe('pdf'); expect(result.current.parameters.isSmartDetection).toBe(true); @@ -118,35 +118,35 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should handle mixed case image extensions', () => { const { result } = renderHook(() => useConvertParameters()); - + const imageFiles = [ { name: 'photo1.JPG' }, { name: 'photo2.PNG' } ]; - + act(() => { result.current.analyzeFileTypes(imageFiles); }); - + expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('images'); }); }); - + describe('Smart Detection - All Web Files', () => { - + test('should detect all web files and enable web smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const webFiles = [ { name: 'page1.html' }, { name: 'archive.zip' } ]; - + act(() => { result.current.analyzeFileTypes(webFiles); }); - + expect(result.current.parameters.fromExtension).toBe('html'); expect(result.current.parameters.toExtension).toBe('pdf'); expect(result.current.parameters.isSmartDetection).toBe(true); @@ -155,54 +155,54 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should handle mixed case web extensions', () => { const { result } = renderHook(() => useConvertParameters()); - + const webFiles = [ { name: 'page1.HTML' }, { name: 'archive.ZIP' } ]; - + act(() => { result.current.analyzeFileTypes(webFiles); }); - + expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('web'); }); test('should detect multiple web files and enable web smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const zipFiles = [ { name: 'site1.zip' }, { name: 'site2.html' } ]; - + act(() => { result.current.analyzeFileTypes(zipFiles); }); - + expect(result.current.parameters.fromExtension).toBe('html'); expect(result.current.parameters.toExtension).toBe('pdf'); expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('web'); }); }); - + describe('Smart Detection - Mixed File Types', () => { - + test('should detect mixed file types and enable smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const mixedFiles = [ { name: 'document.pdf' }, { name: 'spreadsheet.xlsx' }, { name: 'presentation.pptx' } ]; - + act(() => { result.current.analyzeFileTypes(mixedFiles); }); - + expect(result.current.parameters.fromExtension).toBe('any'); expect(result.current.parameters.toExtension).toBe('pdf'); expect(result.current.parameters.isSmartDetection).toBe(true); @@ -211,155 +211,155 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => { test('should detect mixed images and documents as mixed type', () => { const { result } = renderHook(() => useConvertParameters()); - + const mixedFiles = [ { name: 'photo.jpg' }, { name: 'document.pdf' }, { name: 'text.txt' } ]; - + act(() => { result.current.analyzeFileTypes(mixedFiles); }); - + expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('mixed'); }); test('should handle mixed with unknown file types', () => { const { result } = renderHook(() => useConvertParameters()); - + const mixedFiles = [ { name: 'document.pdf' }, { name: 'unknown.xyz' }, { name: 'noextension' } ]; - + act(() => { result.current.analyzeFileTypes(mixedFiles); }); - + expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('mixed'); }); }); - + describe('Smart Detection Endpoint Resolution', () => { - + test('should return correct endpoint for image smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const imageFiles = [ { name: 'photo1.jpg' }, { name: 'photo2.png' } ]; - + act(() => { result.current.analyzeFileTypes(imageFiles); }); - + expect(result.current.getEndpointName()).toBe('img-to-pdf'); expect(result.current.getEndpoint()).toBe('/api/v1/convert/img/pdf'); }); test('should return correct endpoint for web smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const webFiles = [ { name: 'page1.html' }, { name: 'archive.zip' } ]; - + act(() => { result.current.analyzeFileTypes(webFiles); }); - + expect(result.current.getEndpointName()).toBe('html-to-pdf'); expect(result.current.getEndpoint()).toBe('/api/v1/convert/html/pdf'); }); test('should return correct endpoint for mixed smart detection', () => { const { result } = renderHook(() => useConvertParameters()); - + const mixedFiles = [ { name: 'document.pdf' }, { name: 'spreadsheet.xlsx' } ]; - + act(() => { result.current.analyzeFileTypes(mixedFiles); }); - + expect(result.current.getEndpointName()).toBe('file-to-pdf'); expect(result.current.getEndpoint()).toBe('/api/v1/convert/file/pdf'); }); }); - + describe('Auto-Target Selection Logic', () => { - + test('should select single available target automatically', () => { const { result } = renderHook(() => useConvertParameters()); - + // Markdown has only one conversion target (PDF) const mdFile = [{ name: 'readme.md' }]; - + act(() => { result.current.analyzeFileTypes(mdFile); }); - + expect(result.current.parameters.fromExtension).toBe('md'); expect(result.current.parameters.toExtension).toBe('pdf'); // Only available target }); test('should not auto-select when multiple targets available', () => { const { result } = renderHook(() => useConvertParameters()); - + // PDF has multiple conversion targets, so no auto-selection const pdfFile = [{ name: 'document.pdf' }]; - + act(() => { result.current.analyzeFileTypes(pdfFile); }); - + expect(result.current.parameters.fromExtension).toBe('pdf'); // Should NOT auto-select when multiple targets available expect(result.current.parameters.toExtension).toBe(''); }); }); - + describe('Edge Cases', () => { - + test('should handle empty file names', () => { const { result } = renderHook(() => useConvertParameters()); - + const emptyFiles = [{ name: '' }]; - + act(() => { result.current.analyzeFileTypes(emptyFiles); }); - + expect(result.current.parameters.fromExtension).toBe('any'); expect(result.current.parameters.toExtension).toBe('pdf'); }); test('should handle malformed file objects', () => { const { result } = renderHook(() => useConvertParameters()); - - const malformedFiles = [ + + const malformedFiles: Array<{name: string}> = [ { name: 'valid.pdf' }, // @ts-ignore - Testing runtime resilience { name: null }, // @ts-ignore { name: undefined } ]; - + act(() => { result.current.analyzeFileTypes(malformedFiles); }); - + // Should still process the valid file and handle gracefully expect(result.current.parameters.isSmartDetection).toBe(true); expect(result.current.parameters.smartDetectionType).toBe('mixed'); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/hooks/tools/ocr/useOCROperation.ts b/frontend/src/hooks/tools/ocr/useOCROperation.ts index 316b867d83..b684657ad4 100644 --- a/frontend/src/hooks/tools/ocr/useOCROperation.ts +++ b/frontend/src/hooks/tools/ocr/useOCROperation.ts @@ -99,7 +99,7 @@ export const useOCROperation = () => { const ocrConfig: ToolOperationConfig = { operationType: 'ocr', endpoint: '/api/v1/misc/ocr-pdf', - buildFormData, + buildFormData: buildFormData as any /* FIX ME */, filePrefix: 'ocr_', multiFileEndpoint: false, // Process files individually responseHandler, // use shared flow diff --git a/frontend/src/hooks/tools/shared/useToolApiCalls.ts b/frontend/src/hooks/tools/shared/useToolApiCalls.ts index 88f1f4d317..854183d0a3 100644 --- a/frontend/src/hooks/tools/shared/useToolApiCalls.ts +++ b/frontend/src/hooks/tools/shared/useToolApiCalls.ts @@ -1,7 +1,7 @@ import { useCallback, useRef } from 'react'; import axios, { CancelTokenSource } from 'axios'; -import { processResponse } from '../../../utils/toolResponseProcessor'; -import type { ResponseHandler, ProcessingProgress } from './useToolState'; +import { processResponse, ResponseHandler } from '../../../utils/toolResponseProcessor'; +import type { ProcessingProgress } from './useToolState'; export interface ApiCallsConfig { endpoint: string | ((params: TParams) => string); diff --git a/frontend/src/hooks/tools/shared/useToolOperation.ts b/frontend/src/hooks/tools/shared/useToolOperation.ts index 7251d1fd2e..cebdc5a23a 100644 --- a/frontend/src/hooks/tools/shared/useToolOperation.ts +++ b/frontend/src/hooks/tools/shared/useToolOperation.ts @@ -7,7 +7,7 @@ import { useToolApiCalls, type ApiCallsConfig } from './useToolApiCalls'; import { useToolResources } from './useToolResources'; import { extractErrorMessage } from '../../../utils/toolErrorHandler'; import { createOperation } from '../../../utils/toolOperationTracker'; -import { type ResponseHandler, processResponse } from '../../../utils/toolResponseProcessor'; +import { ResponseHandler } from '../../../utils/toolResponseProcessor'; export interface ValidationResult { valid: boolean; @@ -176,7 +176,7 @@ export const useToolOperation = ( } else { // Default: assume ZIP response for multi-file endpoints processedFiles = await extractZipFiles(response.data); - + if (processedFiles.length === 0) { // Try the generic extraction as fallback processedFiles = await extractAllZipFiles(response.data); @@ -186,7 +186,7 @@ export const useToolOperation = ( // Individual file processing - separate API call per file const apiCallsConfig: ApiCallsConfig = { endpoint: config.endpoint, - buildFormData: (file: File, params: TParams) => (config.buildFormData as (file: File, params: TParams) => FormData)(file, params), + buildFormData: (file: File, params: TParams) => (config.buildFormData as any /* FIX ME */)(file, params), filePrefix: config.filePrefix, responseHandler: config.responseHandler }; diff --git a/frontend/src/hooks/tools/shared/useToolResources.ts b/frontend/src/hooks/tools/shared/useToolResources.ts index 69f48fe206..8d0b752852 100644 --- a/frontend/src/hooks/tools/shared/useToolResources.ts +++ b/frontend/src/hooks/tools/shared/useToolResources.ts @@ -36,17 +36,19 @@ export const useToolResources = () => { const generateThumbnails = useCallback(async (files: File[]): Promise => { const thumbnails: string[] = []; - + for (const file of files) { try { const thumbnail = await generateThumbnailForFile(file); - thumbnails.push(thumbnail); + if (thumbnail) { + thumbnails.push(thumbnail); + } } catch (error) { console.warn(`Failed to generate thumbnail for ${file.name}:`, error); thumbnails.push(''); } } - + return thumbnails; }, []); @@ -65,12 +67,12 @@ export const useToolResources = () => { try { const JSZip = (await import('jszip')).default; const zip = new JSZip(); - + const arrayBuffer = await zipBlob.arrayBuffer(); const zipContent = await zip.loadAsync(arrayBuffer); - + const extractedFiles: File[] = []; - + for (const [filename, file] of Object.entries(zipContent.files)) { if (!file.dir) { const content = await file.async('blob'); @@ -78,7 +80,7 @@ export const useToolResources = () => { extractedFiles.push(extractedFile); } } - + return extractedFiles; } catch (error) { console.error('Error in extractAllZipFiles:', error); @@ -87,7 +89,7 @@ export const useToolResources = () => { }, []); const createDownloadInfo = useCallback(async ( - files: File[], + files: File[], operationType: string ): Promise<{ url: string; filename: string }> => { if (files.length === 1) { @@ -100,7 +102,7 @@ export const useToolResources = () => { const { zipFile } = await zipFileService.createZipFromFiles(files, `${operationType}_results.zip`); const url = URL.createObjectURL(zipFile); addBlobUrl(url); - + return { url, filename: zipFile.name }; }, [addBlobUrl]); @@ -111,4 +113,4 @@ export const useToolResources = () => { extractAllZipFiles, cleanupBlobUrls, }; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts index efb6724eb9..dfa2d1dec2 100644 --- a/frontend/src/hooks/useFileManager.ts +++ b/frontend/src/hooks/useFileManager.ts @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { fileStorage } from '../services/fileStorage'; import { FileWithUrl } from '../types/file'; +import { createEnhancedFileFromStored } from '../utils/fileUtils'; import { generateThumbnailForFile } from '../utils/thumbnailUtils'; export const useFileManager = () => { @@ -42,7 +43,7 @@ export const useFileManager = () => { try { const files = await fileStorage.getAllFiles(); const sortedFiles = files.sort((a, b) => (b.lastModified || 0) - (a.lastModified || 0)); - return sortedFiles; + return sortedFiles.map(file => createEnhancedFileFromStored(file)); } catch (error) { console.error('Failed to load recent files:', error); return []; @@ -66,10 +67,10 @@ export const useFileManager = () => { try { // Generate thumbnail for the file const thumbnail = await generateThumbnailForFile(file); - + // Store file with thumbnail const storedFile = await fileStorage.storeFile(file, thumbnail); - + // Add the ID to the file object Object.defineProperty(file, 'id', { value: storedFile.id, writable: false }); return storedFile; @@ -134,4 +135,4 @@ export const useFileManager = () => { touchFile, createFileSelectionHandlers }; -}; \ No newline at end of file +}; diff --git a/frontend/src/hooks/useFileWithUrl.ts b/frontend/src/hooks/useFileWithUrl.ts index aeb954cd1a..fd2f2d604b 100644 --- a/frontend/src/hooks/useFileWithUrl.ts +++ b/frontend/src/hooks/useFileWithUrl.ts @@ -4,25 +4,25 @@ import { useMemo } from 'react'; * Hook to convert a File object to { file: File; url: string } format * Creates blob URL on-demand and handles cleanup */ -export function useFileWithUrl(file: File | null): { file: File; url: string } | null { +export function useFileWithUrl(file: File | Blob | null): { file: File | Blob; url: string } | null { return useMemo(() => { if (!file) return null; - + // Validate that file is a proper File or Blob object if (!(file instanceof File) && !(file instanceof Blob)) { console.warn('useFileWithUrl: Expected File or Blob, got:', file); return null; } - + try { const url = URL.createObjectURL(file); - + // Return object with cleanup function const result = { file, url }; - + // Store cleanup function for later use (result as any)._cleanup = () => URL.revokeObjectURL(url); - + return result; } catch (error) { console.error('useFileWithUrl: Failed to create object URL:', error, file); @@ -40,11 +40,11 @@ export function useFileWithUrlAndCleanup(file: File | null): { } { return useMemo(() => { if (!file) return { fileObj: null, cleanup: () => {} }; - + const url = URL.createObjectURL(file); const fileObj = { file, url }; const cleanup = () => URL.revokeObjectURL(url); - + return { fileObj, cleanup }; }, [file]); -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useIndexedDBThumbnail.ts b/frontend/src/hooks/useIndexedDBThumbnail.ts index b8b2c669ce..03a9e92e9c 100644 --- a/frontend/src/hooks/useIndexedDBThumbnail.ts +++ b/frontend/src/hooks/useIndexedDBThumbnail.ts @@ -10,10 +10,10 @@ import { generateThumbnailForFile } from "../utils/thumbnailUtils"; function calculateThumbnailScale(pageViewport: { width: number; height: number }): number { const maxWidth = 400; // Max thumbnail width const maxHeight = 600; // Max thumbnail height - + const scaleX = maxWidth / pageViewport.width; const scaleY = maxHeight / pageViewport.height; - + // Don't upscale, only downscale if needed return Math.min(scaleX, scaleY, 1.0); } @@ -22,16 +22,16 @@ function calculateThumbnailScale(pageViewport: { width: number; height: number } * Hook for IndexedDB-aware thumbnail loading * Handles thumbnail generation for files not in IndexedDB */ -export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { - thumbnail: string | null; - isGenerating: boolean +export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { + thumbnail: string | null; + isGenerating: boolean } { const [thumb, setThumb] = useState(null); const [generating, setGenerating] = useState(false); useEffect(() => { let cancelled = false; - + async function loadThumbnail() { if (!file) { setThumb(null); @@ -49,7 +49,7 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { setGenerating(true); try { let fileObject: File; - + // Handle IndexedDB files vs regular File objects if (file.storedInIndexedDB && file.id) { // For IndexedDB files, recreate File object from stored data @@ -61,9 +61,9 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { type: storedFile.type, lastModified: storedFile.lastModified }); - } else if (file.file) { + } else if ((file as any /* Fix me */).file) { // For FileWithUrl objects that have a File object - fileObject = file.file; + fileObject = (file as any /* Fix me */).file; } else if (file.id) { // Fallback: try to get from IndexedDB even if storedInIndexedDB flag is missing const storedFile = await fileStorage.getFile(file.id); @@ -77,7 +77,7 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { } else { throw new Error('File object not available and no ID for IndexedDB lookup'); } - + // Use the universal thumbnail generator const thumbnail = await generateThumbnailForFile(fileObject); if (!cancelled && thumbnail) { @@ -102,4 +102,4 @@ export function useIndexedDBThumbnail(file: FileWithUrl | undefined | null): { }, [file, file?.thumbnail, file?.id]); return { thumbnail: thumb, isGenerating: generating }; -} \ No newline at end of file +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index e9009282e1..783a10e9de 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -12,10 +12,10 @@ import FileManager from "../components/FileManager"; function HomePageContent() { - const { - sidebarRefs, + const { + sidebarRefs, } = useSidebarContext(); - + const { quickAccessRef } = sidebarRefs; const { setMaxFiles, setIsToolMode, setSelectedFiles } = useFileSelection(); @@ -44,7 +44,7 @@ function HomePageContent() { ref={quickAccessRef} /> - + ); } @@ -53,7 +53,7 @@ export default function HomePage() { const { setCurrentView } = useFileContext(); return ( - + diff --git a/frontend/src/services/enhancedPDFProcessingService.ts b/frontend/src/services/enhancedPDFProcessingService.ts index 984d106e4f..aa7802084b 100644 --- a/frontend/src/services/enhancedPDFProcessingService.ts +++ b/frontend/src/services/enhancedPDFProcessingService.ts @@ -45,32 +45,32 @@ export class EnhancedPDFProcessingService { */ async processFile(file: File, customConfig?: Partial): Promise { const fileKey = await this.generateFileKey(file); - + // Check cache first const cached = this.cache.get(fileKey); if (cached) { this.updateMetrics('cacheHit'); return cached; } - + // Check if already processing if (this.processing.has(fileKey)) { return null; } - + // Analyze file to determine optimal strategy const analysis = await FileAnalyzer.analyzeFile(file); if (analysis.isCorrupted) { throw new Error(`File ${file.name} appears to be corrupted`); } - + // Create processing config const config: ProcessingConfig = { ...this.defaultConfig, strategy: analysis.recommendedStrategy, ...customConfig }; - + // Start processing this.startProcessing(file, fileKey, config, analysis.estimatedProcessingTime); return null; @@ -80,14 +80,14 @@ export class EnhancedPDFProcessingService { * Start processing a file with the specified configuration */ private async startProcessing( - file: File, - fileKey: string, + file: File, + fileKey: string, config: ProcessingConfig, estimatedTime: number ): Promise { // Create cancellation token const cancellationToken = new AbortController(); - + // Set initial state const state: ProcessingState = { fileKey, @@ -99,7 +99,7 @@ export class EnhancedPDFProcessingService { estimatedTimeRemaining: estimatedTime, cancellationToken }; - + this.processing.set(fileKey, state); this.notifyListeners(); this.updateMetrics('started'); @@ -117,14 +117,14 @@ export class EnhancedPDFProcessingService { // Cache the result this.cache.set(fileKey, processedFile); - + // Update state to completed state.status = 'completed'; state.progress = 100; state.completedAt = Date.now(); this.notifyListeners(); this.updateMetrics('completed', Date.now() - state.startedAt); - + // Remove from processing map after brief delay setTimeout(() => { this.processing.delete(fileKey); @@ -133,13 +133,13 @@ export class EnhancedPDFProcessingService { } catch (error) { console.error('Processing failed for', file.name, ':', error); - + const processingError = ProcessingErrorHandler.createProcessingError(error); state.status = 'error'; state.error = processingError; this.notifyListeners(); this.updateMetrics('failed'); - + // Remove failed processing after delay setTimeout(() => { this.processing.delete(fileKey); @@ -152,23 +152,23 @@ export class EnhancedPDFProcessingService { * Execute the actual processing based on strategy */ private async executeProcessingStrategy( - file: File, - config: ProcessingConfig, + file: File, + config: ProcessingConfig, state: ProcessingState ): Promise { switch (config.strategy) { case 'immediate_full': return this.processImmediateFull(file, config, state); - + case 'priority_pages': return this.processPriorityPages(file, config, state); - + case 'progressive_chunked': return this.processProgressiveChunked(file, config, state); - + case 'metadata_only': return this.processMetadataOnly(file, config, state); - + default: return this.processImmediateFull(file, config, state); } @@ -178,29 +178,29 @@ export class EnhancedPDFProcessingService { * Process all pages immediately (for small files) */ private async processImmediateFull( - file: File, - config: ProcessingConfig, + file: File, + config: ProcessingConfig, state: ProcessingState ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; - + state.progress = 10; this.notifyListeners(); - + const pages: PDFPage[] = []; - + for (let i = 1; i <= totalPages; i++) { // Check for cancellation if (state.cancellationToken?.signal.aborted) { pdf.destroy(); throw new Error('Processing cancelled'); } - + const page = await pdf.getPage(i); const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); - + pages.push({ id: `${file.name}-page-${i}`, pageNumber: i, @@ -208,17 +208,17 @@ export class EnhancedPDFProcessingService { rotation: 0, selected: false }); - + // Update progress state.progress = 10 + (i / totalPages) * 85; state.currentPage = i; this.notifyListeners(); } - + pdf.destroy(); state.progress = 100; this.notifyListeners(); - + return this.createProcessedFile(file, pages, totalPages); } @@ -226,30 +226,30 @@ export class EnhancedPDFProcessingService { * Process priority pages first, then queue the rest */ private async processPriorityPages( - file: File, - config: ProcessingConfig, + file: File, + config: ProcessingConfig, state: ProcessingState ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; - + state.progress = 10; this.notifyListeners(); - + const pages: PDFPage[] = []; const priorityCount = Math.min(config.priorityPageCount, totalPages); - + // Process priority pages first for (let i = 1; i <= priorityCount; i++) { if (state.cancellationToken?.signal.aborted) { pdf.destroy(); throw new Error('Processing cancelled'); } - + const page = await pdf.getPage(i); const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); - + pages.push({ id: `${file.name}-page-${i}`, pageNumber: i, @@ -257,12 +257,12 @@ export class EnhancedPDFProcessingService { rotation: 0, selected: false }); - + state.progress = 10 + (i / priorityCount) * 60; state.currentPage = i; this.notifyListeners(); } - + // Create placeholder pages for remaining pages for (let i = priorityCount + 1; i <= totalPages; i++) { pages.push({ @@ -273,11 +273,11 @@ export class EnhancedPDFProcessingService { selected: false }); } - + pdf.destroy(); state.progress = 100; this.notifyListeners(); - + return this.createProcessedFile(file, pages, totalPages); } @@ -285,33 +285,33 @@ export class EnhancedPDFProcessingService { * Process in chunks with breaks between chunks */ private async processProgressiveChunked( - file: File, - config: ProcessingConfig, + file: File, + config: ProcessingConfig, state: ProcessingState ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; - + state.progress = 10; this.notifyListeners(); - + const pages: PDFPage[] = []; const chunkSize = config.chunkSize; let processedPages = 0; - + // Process first chunk immediately const firstChunkEnd = Math.min(chunkSize, totalPages); - + for (let i = 1; i <= firstChunkEnd; i++) { if (state.cancellationToken?.signal.aborted) { pdf.destroy(); throw new Error('Processing cancelled'); } - + const page = await pdf.getPage(i); const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality); - + pages.push({ id: `${file.name}-page-${i}`, pageNumber: i, @@ -319,18 +319,18 @@ export class EnhancedPDFProcessingService { rotation: 0, selected: false }); - + processedPages++; state.progress = 10 + (processedPages / totalPages) * 70; state.currentPage = i; this.notifyListeners(); - + // Small delay to prevent UI blocking if (i % 5 === 0) { await new Promise(resolve => setTimeout(resolve, 10)); } } - + // Create placeholders for remaining pages for (let i = firstChunkEnd + 1; i <= totalPages; i++) { pages.push({ @@ -341,11 +341,11 @@ export class EnhancedPDFProcessingService { selected: false }); } - + pdf.destroy(); state.progress = 100; this.notifyListeners(); - + return this.createProcessedFile(file, pages, totalPages); } @@ -353,17 +353,17 @@ export class EnhancedPDFProcessingService { * Process metadata only (for very large files) */ private async processMetadataOnly( - file: File, - config: ProcessingConfig, + file: File, + config: ProcessingConfig, state: ProcessingState ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; - + state.progress = 50; this.notifyListeners(); - + // Create placeholder pages without thumbnails const pages: PDFPage[] = []; for (let i = 1; i <= totalPages; i++) { @@ -375,11 +375,11 @@ export class EnhancedPDFProcessingService { selected: false }); } - + pdf.destroy(); state.progress = 100; this.notifyListeners(); - + return this.createProcessedFile(file, pages, totalPages); } @@ -389,17 +389,17 @@ export class EnhancedPDFProcessingService { private async renderPageThumbnail(page: any, quality: 'low' | 'medium' | 'high'): Promise { const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor const scale = scales[quality]; - + const viewport = page.getViewport({ scale }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; - + const context = canvas.getContext('2d'); if (!context) { throw new Error('Could not get canvas context'); } - + await page.render({ canvasContext: context, viewport }).promise; return canvas.toDataURL('image/jpeg', 0.8); // Use JPEG for better compression } @@ -513,14 +513,15 @@ export class EnhancedPDFProcessingService { state.cancellationToken.abort(); } }); - + // Clear processing states this.processing.clear(); this.notifyListeners(); - + // Force memory cleanup hint if (typeof window !== 'undefined' && window.gc) { - setTimeout(() => window.gc(), 100); + let gc = window.gc; + setTimeout(() => gc(), 100); } } @@ -542,4 +543,4 @@ export class EnhancedPDFProcessingService { } // Export singleton instance -export const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance(); \ No newline at end of file +export const enhancedPDFProcessingService = EnhancedPDFProcessingService.getInstance(); diff --git a/frontend/src/services/fileAnalyzer.ts b/frontend/src/services/fileAnalyzer.ts index 2a9f15cff2..6c5f3ec1ff 100644 --- a/frontend/src/services/fileAnalyzer.ts +++ b/frontend/src/services/fileAnalyzer.ts @@ -36,11 +36,11 @@ export class FileAnalyzer { // Determine strategy based on file characteristics analysis.recommendedStrategy = this.determineStrategy(file.size, quickAnalysis.pageCount); - + // Estimate processing time analysis.estimatedProcessingTime = this.estimateProcessingTime( - file.size, - quickAnalysis.pageCount, + file.size, + quickAnalysis.pageCount, analysis.recommendedStrategy ); @@ -66,15 +66,15 @@ export class FileAnalyzer { // For large files, try the whole file first (PDF.js needs the complete structure) const arrayBuffer = await file.arrayBuffer(); - const pdf = await getDocument({ + const pdf = await getDocument({ data: arrayBuffer, stopAtErrors: false, // Don't stop at minor errors verbosity: 0 // Suppress PDF.js warnings }).promise; const pageCount = pdf.numPages; - const isEncrypted = pdf.isEncrypted; - + const isEncrypted = (pdf as any).isEncrypted; + // Clean up pdf.destroy(); @@ -88,7 +88,7 @@ export class FileAnalyzer { // Try to determine if it's corruption vs encryption const errorMessage = error instanceof Error ? error.message.toLowerCase() : ''; const isEncrypted = errorMessage.includes('password') || errorMessage.includes('encrypted'); - + return { pageCount: 0, isEncrypted, @@ -129,8 +129,8 @@ export class FileAnalyzer { * Estimate processing time based on file characteristics and strategy */ private static estimateProcessingTime( - fileSize: number, - pageCount: number = 0, + fileSize: number, + pageCount: number = 0, strategy: ProcessingStrategy ): number { const baseTimes = { @@ -145,20 +145,20 @@ export class FileAnalyzer { switch (strategy) { case 'metadata_only': return baseTime; - + case 'immediate_full': return pageCount * baseTime; - + case 'priority_pages': // Estimate time for priority pages (first 10) const priorityPages = Math.min(pageCount, 10); return priorityPages * baseTime; - + case 'progressive_chunked': // Estimate time for first chunk (20 pages) const firstChunk = Math.min(pageCount, 20); return firstChunk * baseTime; - + default: return pageCount * baseTime; } @@ -209,11 +209,11 @@ export class FileAnalyzer { if (totalSize > this.SIZE_THRESHOLDS.LARGE) { return Math.max(1, Math.floor(fileCount / 4)); } - + if (totalSize > this.SIZE_THRESHOLDS.MEDIUM) { return Math.max(2, Math.floor(fileCount / 2)); } - + // Process all at once for smaller total sizes return fileCount; } @@ -231,10 +231,10 @@ export class FileAnalyzer { const header = file.slice(0, 8); const headerBytes = new Uint8Array(await header.arrayBuffer()); const headerString = String.fromCharCode(...headerBytes); - + return headerString.startsWith('%PDF-'); } catch (error) { return false; } } -} \ No newline at end of file +} diff --git a/frontend/src/services/fileStorage.ts b/frontend/src/services/fileStorage.ts index 5fd5739e83..1e7a73ee53 100644 --- a/frontend/src/services/fileStorage.ts +++ b/frontend/src/services/fileStorage.ts @@ -35,11 +35,11 @@ class FileStorageService { if (this.db) { return Promise.resolve(); } - + if (this.initPromise) { return this.initPromise; } - + this.initPromise = new Promise((resolve, reject) => { const request = indexedDB.open(this.dbName, this.dbVersion); @@ -47,7 +47,7 @@ class FileStorageService { this.initPromise = null; reject(request.error); }; - + request.onsuccess = () => { this.db = request.result; console.log('IndexedDB connection established'); @@ -57,9 +57,9 @@ class FileStorageService { request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; const oldVersion = (event as any).oldVersion; - + console.log('IndexedDB upgrade needed from version', oldVersion, 'to', this.dbVersion); - + // Only recreate object store if it doesn't exist or if upgrading from version < 2 if (!db.objectStoreNames.contains(this.storeName)) { const store = db.createObjectStore(this.storeName, { keyPath: 'id' }); @@ -76,7 +76,7 @@ class FileStorageService { } }; }); - + return this.initPromise; } @@ -88,7 +88,7 @@ class FileStorageService { const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const arrayBuffer = await file.arrayBuffer(); - + const storedFile: StoredFile = { id, name: file.name, @@ -103,16 +103,16 @@ class FileStorageService { try { const transaction = this.db!.transaction([this.storeName], 'readwrite'); const store = transaction.objectStore(this.storeName); - + // Debug logging console.log('Object store keyPath:', store.keyPath); - console.log('Storing file:', { - id: storedFile.id, - name: storedFile.name, + console.log('Storing file:', { + id: storedFile.id, + name: storedFile.name, hasData: !!storedFile.data, - dataSize: storedFile.data.byteLength + dataSize: storedFile.data.byteLength }); - + const request = store.add(storedFile); request.onerror = () => { @@ -161,10 +161,10 @@ class FileStorageService { request.onerror = () => reject(request.error); request.onsuccess = () => { // Filter out null/corrupted entries - const files = request.result.filter(file => - file && - file.data && - file.name && + const files = request.result.filter(file => + file && + file.data && + file.name && typeof file.size === 'number' ); resolve(files); @@ -277,7 +277,7 @@ class FileStorageService { let available = 0; let quota: number | undefined; let fileCount = 0; - + try { // Get browser quota for context if ('storage' in navigator && 'estimate' in navigator.storage) { @@ -285,17 +285,17 @@ class FileStorageService { quota = estimate.quota; available = estimate.quota || 0; } - + // Calculate our actual IndexedDB usage from file metadata const files = await this.getAllFileMetadata(); used = files.reduce((total, file) => total + (file?.size || 0), 0); fileCount = files.length; - + // Adjust available space if (quota) { available = quota - used; } - + } catch (error) { console.warn('Could not get storage stats:', error); // If we can't read metadata, database might be purged @@ -332,12 +332,12 @@ class FileStorageService { */ async debugAllDatabases(): Promise { console.log('=== Checking All IndexedDB Databases ==='); - + if ('databases' in indexedDB) { try { const databases = await indexedDB.databases(); console.log('Found databases:', databases); - + for (const dbInfo of databases) { if (dbInfo.name?.includes('stirling') || dbInfo.name?.includes('pdf')) { console.log(`Checking database: ${dbInfo.name} (version: ${dbInfo.version})`); @@ -347,7 +347,7 @@ class FileStorageService { request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); - + console.log(`Database ${dbInfo.name} object stores:`, Array.from(db.objectStoreNames)); db.close(); } catch (error) { @@ -361,7 +361,7 @@ class FileStorageService { } else { console.log('indexedDB.databases() not supported'); } - + // Also check our specific database with different versions for (let version = 1; version <= 3; version++) { try { @@ -375,9 +375,9 @@ class FileStorageService { request.transaction?.abort(); }; }); - + console.log(`Version ${version} object stores:`, Array.from(db.objectStoreNames)); - + if (db.objectStoreNames.contains('files')) { const transaction = db.transaction(['files'], 'readonly'); const store = transaction.objectStore('files'); @@ -386,10 +386,12 @@ class FileStorageService { console.log(`Version ${version} files store has ${countRequest.result} entries`); }; } - + db.close(); } catch (error) { - console.log(`Version ${version} not accessible:`, error.message); + if (error instanceof Error) { + console.log(`Version ${version} not accessible:`, error.message); + } } } } @@ -403,7 +405,7 @@ class FileStorageService { return new Promise((resolve, reject) => { const transaction = this.db!.transaction([this.storeName], 'readonly'); const store = transaction.objectStore(this.storeName); - + // First try getAll to see if there's anything const getAllRequest = store.getAll(); getAllRequest.onsuccess = () => { @@ -422,7 +424,7 @@ class FileStorageService { }); }); }; - + // Then try cursor const cursorRequest = store.openCursor(); console.log('=== IndexedDB Cursor Debug ==='); @@ -432,7 +434,7 @@ class FileStorageService { console.error('Cursor error:', cursorRequest.error); reject(cursorRequest.error); }; - + cursorRequest.onsuccess = (event) => { const cursor = (event.target as IDBRequest).result; if (cursor) { @@ -464,21 +466,21 @@ class FileStorageService { if (!storedFile || !storedFile.data) { throw new Error('Invalid stored file: missing data'); } - + if (!storedFile.name || typeof storedFile.size !== 'number') { throw new Error('Invalid stored file: missing metadata'); } - + const blob = new Blob([storedFile.data], { type: storedFile.type }); const file = new File([blob], storedFile.name, { type: storedFile.type, lastModified: storedFile.lastModified }); - + // Add custom properties for compatibility Object.defineProperty(file, 'id', { value: storedFile.id, writable: false }); Object.defineProperty(file, 'thumbnail', { value: storedFile.thumbnail, writable: false }); - + return file; } @@ -509,15 +511,15 @@ class FileStorageService { async createTemporaryBlobUrl(id: string): Promise { const data = await this.getFileData(id); if (!data) return null; - + const blob = new Blob([data], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); - + // Auto-revoke after a short delay to free memory setTimeout(() => { URL.revokeObjectURL(url); }, 10000); // 10 seconds - + return url; } @@ -538,7 +540,7 @@ class FileStorageService { if (storedFile) { storedFile.thumbnail = thumbnail; const updateRequest = store.put(storedFile); - + updateRequest.onsuccess = () => { console.log('Thumbnail updated for file:', id); resolve(true); @@ -569,7 +571,7 @@ class FileStorageService { async isStorageLow(): Promise { const stats = await this.getStorageStats(); if (!stats.quota) return false; - + const usagePercent = stats.used / stats.quota; return usagePercent > 0.8; // Consider low if over 80% used } @@ -579,12 +581,12 @@ class FileStorageService { */ async cleanupOldFiles(maxFiles: number = 50): Promise { const files = await this.getAllFileMetadata(); - + if (files.length <= maxFiles) return; - + // Sort by last modified (oldest first) files.sort((a, b) => a.lastModified - b.lastModified); - + // Delete oldest files const filesToDelete = files.slice(0, files.length - maxFiles); for (const file of filesToDelete) { @@ -599,4 +601,4 @@ export const fileStorage = new FileStorageService(); // Helper hook for React components export function useFileStorage() { return fileStorage; -} \ No newline at end of file +} diff --git a/frontend/src/services/pdfProcessingService.ts b/frontend/src/services/pdfProcessingService.ts index 5bb6f2ce37..2b0dae0f03 100644 --- a/frontend/src/services/pdfProcessingService.ts +++ b/frontend/src/services/pdfProcessingService.ts @@ -22,20 +22,20 @@ export class PDFProcessingService { async getProcessedFile(file: File): Promise { const fileKey = this.generateFileKey(file); - + // Check cache first const cached = this.cache.get(fileKey); if (cached) { console.log('Cache hit for:', file.name); return cached; } - + // Check if already processing if (this.processing.has(fileKey)) { console.log('Already processing:', file.name); return null; // Will be available when processing completes } - + // Start processing this.startProcessing(file, fileKey); return null; @@ -48,9 +48,10 @@ export class PDFProcessingService { fileName: file.name, status: 'processing', progress: 0, - startedAt: Date.now() + startedAt: Date.now(), + strategy: 'immediate_full' }; - + this.processing.set(fileKey, state); this.notifyListeners(); @@ -63,13 +64,13 @@ export class PDFProcessingService { // Cache the result this.cache.set(fileKey, processedFile); - + // Update state to completed state.status = 'completed'; state.progress = 100; state.completedAt = Date.now(); this.notifyListeners(); - + // Remove from processing map after brief delay setTimeout(() => { this.processing.delete(fileKey); @@ -79,9 +80,9 @@ export class PDFProcessingService { } catch (error) { console.error('Processing failed for', file.name, ':', error); state.status = 'error'; - state.error = error instanceof Error ? error.message : 'Unknown error'; + state.error = (error instanceof Error ? error.message : 'Unknown error') as any; this.notifyListeners(); - + // Remove failed processing after delay setTimeout(() => { this.processing.delete(fileKey); @@ -91,29 +92,29 @@ export class PDFProcessingService { } private async processFileWithProgress( - file: File, + file: File, onProgress: (progress: number) => void ): Promise { const arrayBuffer = await file.arrayBuffer(); const pdf = await getDocument({ data: arrayBuffer }).promise; const totalPages = pdf.numPages; - + onProgress(10); // PDF loaded - + const pages: PDFPage[] = []; - + for (let i = 1; i <= totalPages; i++) { const page = await pdf.getPage(i); const viewport = page.getViewport({ scale: 0.5 }); const canvas = document.createElement('canvas'); canvas.width = viewport.width; canvas.height = viewport.height; - + const context = canvas.getContext('2d'); if (context) { await page.render({ canvasContext: context, viewport }).promise; const thumbnail = canvas.toDataURL(); - + pages.push({ id: `${file.name}-page-${i}`, pageNumber: i, @@ -122,15 +123,15 @@ export class PDFProcessingService { selected: false }); } - + // Update progress const progress = 10 + (i / totalPages) * 85; // 10-95% onProgress(progress); } - + pdf.destroy(); onProgress(100); - + return { id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, pages, @@ -185,4 +186,4 @@ export class PDFProcessingService { } // Export singleton instance -export const pdfProcessingService = PDFProcessingService.getInstance(); \ No newline at end of file +export const pdfProcessingService = PDFProcessingService.getInstance(); diff --git a/frontend/src/services/zipFileService.ts b/frontend/src/services/zipFileService.ts index 90f5b25744..0b706cd96e 100644 --- a/frontend/src/services/zipFileService.ts +++ b/frontend/src/services/zipFileService.ts @@ -1,4 +1,17 @@ -import JSZip from 'jszip'; +import JSZip, { JSZipObject } from 'jszip'; + +// Undocumented interface in JSZip for JSZipObject._data +interface CompressedObject { + compressedSize: number; + uncompressedSize: number; + crc32: number; + compression: object; + compressedContent: string|ArrayBuffer|Uint8Array|Buffer; +} + +const getData = (zipEntry: JSZipObject): CompressedObject | undefined => { + return (zipEntry as any)._data as CompressedObject; +} export interface ZipExtractionResult { success: boolean; @@ -68,7 +81,7 @@ export class ZipFileService { } fileCount++; - const uncompressedSize = zipEntry._data?.uncompressedSize || 0; + const uncompressedSize = getData(zipEntry)?.uncompressedSize || 0; totalSize += uncompressedSize; // Check if file is a PDF @@ -109,25 +122,25 @@ export class ZipFileService { async createZipFromFiles(files: File[], zipFilename: string): Promise<{ zipFile: File; size: number }> { try { const zip = new JSZip(); - + // Add each file to the ZIP for (const file of files) { const content = await file.arrayBuffer(); zip.file(file.name, content); } - + // Generate ZIP blob - const zipBlob = await zip.generateAsync({ + const zipBlob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE', compressionOptions: { level: 6 } }); - - const zipFile = new File([zipBlob], zipFilename, { + + const zipFile = new File([zipBlob], zipFilename, { type: 'application/zip', lastModified: Date.now() }); - + return { zipFile, size: zipFile.size }; } catch (error) { throw new Error(`Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -162,7 +175,7 @@ export class ZipFileService { const zipContents = await zip.loadAsync(file); // Get all PDF files - const pdfFiles = Object.entries(zipContents.files).filter(([filename, zipEntry]) => + const pdfFiles = Object.entries(zipContents.files).filter(([filename, zipEntry]) => !zipEntry.dir && this.isPdfFile(filename) ); @@ -171,7 +184,7 @@ export class ZipFileService { // Extract each PDF file for (let i = 0; i < pdfFiles.length; i++) { const [filename, zipEntry] = pdfFiles[i]; - + try { // Report progress if (onProgress) { @@ -185,9 +198,9 @@ export class ZipFileService { // Extract file content const content = await zipEntry.async('uint8array'); - + // Create File object - const extractedFile = new File([content], this.sanitizeFilename(filename), { + const extractedFile = new File([content as any], this.sanitizeFilename(filename), { type: 'application/pdf', lastModified: zipEntry.date?.getTime() || Date.now() }); @@ -235,7 +248,7 @@ export class ZipFileService { const validExtensions = ['.zip']; const hasValidType = validTypes.includes(file.type); - const hasValidExtension = validExtensions.some(ext => + const hasValidExtension = validExtensions.some(ext => file.name.toLowerCase().endsWith(ext) ); @@ -257,7 +270,7 @@ export class ZipFileService { // Read first few bytes to check PDF header const buffer = await file.slice(0, 8).arrayBuffer(); const bytes = new Uint8Array(buffer); - + // Check for PDF header: %PDF- return bytes[0] === 0x25 && // % bytes[1] === 0x50 && // P @@ -275,7 +288,7 @@ export class ZipFileService { private sanitizeFilename(filename: string): string { // Remove directory path and get just the filename const basename = filename.split('/').pop() || filename; - + // Remove or replace unsafe characters return basename .replace(/[<>:"/\\|?*]/g, '_') // Replace unsafe chars with underscore @@ -309,15 +322,15 @@ export class ZipFileService { try { const zip = new JSZip(); await zip.loadAsync(file); - + // Check if any files are encrypted for (const [filename, zipEntry] of Object.entries(zip.files)) { - if (zipEntry.options?.compression === 'STORE' && zipEntry._data?.compressedSize === 0) { + if (zipEntry.options?.compression === 'STORE' && getData(zipEntry)?.compressedSize === 0) { // This might indicate encryption, but JSZip doesn't provide direct encryption detection // We'll handle this in the extraction phase } } - + return false; // JSZip will throw an error if password is required } catch (error) { // If we can't load the ZIP, it might be password protected @@ -328,4 +341,4 @@ export class ZipFileService { } // Export singleton instance -export const zipFileService = new ZipFileService(); \ No newline at end of file +export const zipFileService = new ZipFileService(); diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 0f5ca8648a..3b406a91e6 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -75,7 +75,7 @@ Object.defineProperty(globalThis, 'crypto', { } return array; }), - } as Crypto, + } as unknown as Crypto, writable: true, configurable: true, }); @@ -120,4 +120,4 @@ Object.defineProperty(window, 'matchMedia', { }) // Set global test timeout to prevent hangs -vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 }) \ No newline at end of file +vi.setConfig({ testTimeout: 5000, hookTimeout: 5000 }) diff --git a/frontend/src/tests/convert/ConvertE2E.spec.ts b/frontend/src/tests/convert/ConvertE2E.spec.ts index 90d203b555..60e2c4849a 100644 --- a/frontend/src/tests/convert/ConvertE2E.spec.ts +++ b/frontend/src/tests/convert/ConvertE2E.spec.ts @@ -1,16 +1,16 @@ /** * End-to-End Tests for Convert Tool - * + * * These tests dynamically discover available conversion endpoints and test them. * Tests are automatically skipped if the backend endpoint is not available. - * + * * Run with: npm run test:e2e or npx playwright test */ import { test, expect, Page } from '@playwright/test'; -import { - conversionDiscovery, - type ConversionEndpoint +import { + conversionDiscovery, + type ConversionEndpoint } from '../helpers/conversionEndpointDiscovery'; import * as path from 'path'; import * as fs from 'fs'; @@ -25,25 +25,25 @@ const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080'; */ function resolveTestFixturePath(filename: string): string { const cwd = process.cwd(); - + // Try frontend/src/tests/test-fixtures/ first (from top-level) const topLevelPath = path.join(cwd, 'frontend', 'src', 'tests', 'test-fixtures', filename); if (fs.existsSync(topLevelPath)) { return topLevelPath; } - + // Try src/tests/test-fixtures/ (from frontend directory) const frontendPath = path.join(cwd, 'src', 'tests', 'test-fixtures', filename); if (fs.existsSync(frontendPath)) { return frontendPath; } - + // Try relative path from current test file location const relativePath = path.join(__dirname, '..', 'test-fixtures', filename); if (fs.existsSync(relativePath)) { return relativePath; } - + // Fallback to the original path format (should work from top-level) return path.join('.', 'frontend', 'src', 'tests', 'test-fixtures', filename); } @@ -98,7 +98,7 @@ const getTestFileForFormat = (format: string): string => { 'xml': TEST_FILES.xml, 'csv': TEST_FILES.csv }; - + return formatMap[format] || TEST_FILES.pdf; // Fallback to PDF }; @@ -123,7 +123,7 @@ const getExpectedExtension = (toFormat: string): string => { 'webp': '.webp', 'pdfa': '.pdf' }; - + return extensionMap[toFormat] || '.pdf'; }; @@ -133,17 +133,17 @@ const getExpectedExtension = (toFormat: string): string => { async function uploadFileViaModal(page: Page, filePath: string) { // Click the Files button in the QuickAccessBar to open the modal await page.click('[data-testid="files-button"]'); - + // Wait for the modal to open - await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible' }, { timeout: 5000 }); + await page.waitForSelector('.mantine-Modal-overlay', { state: 'visible', timeout: 5000 }); //await page.waitForSelector('[data-testid="file-upload-modal"]', { timeout: 5000 }); - + // Upload the file through the modal's file input await page.setInputFiles('input[type="file"]', filePath); - + // Wait for the file to be processed and the modal to close await page.waitForSelector('[data-testid="file-upload-modal"]', { state: 'hidden' }); - + // Wait for the file thumbnail to appear in the main interface await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 }); } @@ -153,33 +153,33 @@ async function uploadFileViaModal(page: Page, filePath: string) { */ async function testConversion(page: Page, conversion: ConversionEndpoint) { const expectedExtension = getExpectedExtension(conversion.toFormat); - + console.log(`Testing ${conversion.endpoint}: ${conversion.fromFormat} → ${conversion.toFormat}`); - + // File should already be uploaded, click the Convert tool button await page.click('[data-testid="tool-convert"]'); - + // Wait for the FileEditor to load in convert mode with file thumbnails await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 }); - + // Click the file thumbnail checkbox to select it in the FileEditor await page.click('[data-testid="file-thumbnail-checkbox"]'); - + // Wait for the conversion settings to appear after file selection await page.waitForSelector('[data-testid="convert-from-dropdown"]', { timeout: 5000 }); - + // Select FROM format await page.click('[data-testid="convert-from-dropdown"]'); const fromFormatOption = page.locator(`[data-testid="format-option-${conversion.fromFormat}"]`); await fromFormatOption.scrollIntoViewIfNeeded(); await fromFormatOption.click(); - + // Select TO format await page.click('[data-testid="convert-to-dropdown"]'); const toFormatOption = page.locator(`[data-testid="format-option-${conversion.toFormat}"]`); await toFormatOption.scrollIntoViewIfNeeded(); await toFormatOption.click(); - + // Handle format-specific options if (conversion.toFormat === 'image' || ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'webp'].includes(conversion.toFormat)) { // Set image conversion options if they appear @@ -188,17 +188,17 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) { // Click the color type dropdown and select "Color" await page.click('[data-testid="color-type-select"]'); await page.getByRole('option', { name: 'Color' }).click(); - + // Set DPI value await page.fill('[data-testid="dpi-input"]', '150'); - + // Click the output type dropdown and select "Multiple" await page.click('[data-testid="output-type-select"]'); - + await page.getByRole('option', { name: 'single' }).click(); } } - + if (conversion.fromFormat === 'image' && conversion.toFormat === 'pdf') { // Set PDF creation options if they appear const pdfOptionsVisible = await page.locator('[data-testid="pdf-options-section"]').isVisible().catch(() => false); @@ -208,7 +208,7 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) { await page.locator('[data-value="color"]').click(); } } - + if (conversion.fromFormat === 'pdf' && conversion.toFormat === 'csv') { // Set CSV extraction options if they appear const csvOptionsVisible = await page.locator('[data-testid="csv-options-section"]').isVisible().catch(() => false); @@ -217,32 +217,32 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) { await page.fill('[data-testid="page-numbers-input"]', '1-2'); } } - + // Start conversion await page.click('[data-testid="convert-button"]'); - + // Wait for conversion to complete (with generous timeout) await page.waitForSelector('[data-testid="download-button"]', { timeout: 60000 }); - + // Verify download is available const downloadButton = page.locator('[data-testid="download-button"]'); await expect(downloadButton).toBeVisible(); - + // Start download and verify file const downloadPromise = page.waitForEvent('download'); await downloadButton.click(); const download = await downloadPromise; - + // Verify file extension expect(download.suggestedFilename()).toMatch(new RegExp(`\\${expectedExtension}$`)); - + // Save and verify file is not empty const path = await download.path(); if (path) { const fs = require('fs'); const stats = fs.statSync(path); expect(stats.size).toBeGreaterThan(0); - + // Format-specific validations if (conversion.toFormat === 'pdf' || conversion.toFormat === 'pdfa') { // Verify PDF header @@ -250,13 +250,13 @@ async function testConversion(page: Page, conversion: ConversionEndpoint) { const header = buffer.toString('utf8', 0, 4); expect(header).toBe('%PDF'); } - + if (conversion.toFormat === 'txt') { // Verify text content exists const content = fs.readFileSync(path, 'utf8'); expect(content.length).toBeGreaterThan(0); } - + if (conversion.toFormat === 'csv') { // Verify CSV content contains separators const content = fs.readFileSync(path, 'utf8'); @@ -282,18 +282,18 @@ let unavailableConversions: ConversionEndpoint[] = []; })(); test.describe('Convert Tool E2E Tests', () => { - + test.beforeAll(async () => { // Re-discover to ensure fresh data at test time console.log('Re-discovering available conversion endpoints...'); availableConversions = await conversionDiscovery.getAvailableConversions(); unavailableConversions = await conversionDiscovery.getUnavailableConversions(); - + console.log(`Found ${availableConversions.length} available conversions:`); availableConversions.forEach(conv => { console.log(` ✓ ${conv.endpoint}: ${conv.fromFormat} → ${conv.toFormat}`); }); - + if (unavailableConversions.length > 0) { console.log(`Found ${unavailableConversions.length} unavailable conversions:`); unavailableConversions.forEach(conv => { @@ -301,136 +301,190 @@ test.describe('Convert Tool E2E Tests', () => { }); } }); - + test.beforeEach(async ({ page }) => { - // Navigate to the homepage + // Navigate to the homepage await page.goto(`${BASE_URL}`); - + // Wait for the page to load await page.waitForLoadState('networkidle'); - + // Wait for the QuickAccessBar to appear await page.waitForSelector('[data-testid="files-button"]', { timeout: 10000 }); }); test.describe('Dynamic Conversion Tests', () => { - + // Generate a test for each potentially available conversion // We'll discover all possible conversions and then skip unavailable ones at runtime test('PDF to PNG conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/img', fromFormat: 'pdf', toFormat: 'png' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/img', + fromFormat: 'pdf', + toFormat: 'png', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to DOCX conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/word', fromFormat: 'pdf', toFormat: 'docx' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/word', + fromFormat: 'pdf', + toFormat: 'docx', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('DOCX to PDF conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/file/pdf', fromFormat: 'docx', toFormat: 'pdf' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/file/pdf', + fromFormat: 'docx', + toFormat: 'pdf', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('Image to PDF conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/img/pdf', fromFormat: 'png', toFormat: 'pdf' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/img/pdf', + fromFormat: 'png', + toFormat: 'pdf', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to TXT conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/text', fromFormat: 'pdf', toFormat: 'txt' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/text', + fromFormat: 'pdf', + toFormat: 'txt', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to HTML conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/html', fromFormat: 'pdf', toFormat: 'html' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/html', + fromFormat: 'pdf', + toFormat: 'html', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to XML conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/xml', fromFormat: 'pdf', toFormat: 'xml' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/xml', + fromFormat: 'pdf', + toFormat: 'xml', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to CSV conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/csv', fromFormat: 'pdf', toFormat: 'csv' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/csv', + fromFormat: 'pdf', + toFormat: 'csv', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); test('PDF to PDFA conversion', async ({ page }) => { - const conversion = { endpoint: '/api/v1/convert/pdf/pdfa', fromFormat: 'pdf', toFormat: 'pdfa' }; + const conversion: ConversionEndpoint = { + endpoint: '/api/v1/convert/pdf/pdfa', + fromFormat: 'pdf', + toFormat: 'pdfa', + description: '', + apiPath: '' + }; const isAvailable = availableConversions.some(c => c.apiPath === conversion.endpoint); test.skip(!isAvailable, `Endpoint ${conversion.endpoint} is not available`); - + const testFile = getTestFileForFormat(conversion.fromFormat); await uploadFileViaModal(page, testFile); - + await testConversion(page, conversion); }); }); test.describe('Static Tests', () => { - + // Test that disabled conversions don't appear in dropdowns when they shouldn't test('should not show conversion button when no valid conversions available', async ({ page }) => { // This test ensures the convert button is disabled when no valid conversion is possible await uploadFileViaModal(page, TEST_FILES.pdf); - + // Click the Convert tool button await page.click('[data-testid="tool-convert"]'); - + // Wait for convert mode and select file await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 }); await page.click('[data-testid="file-thumbnail-checkbox"]'); - + // Don't select any formats - convert button should not exist const convertButton = page.locator('[data-testid="convert-button"]'); await expect(convertButton).toHaveCount(0); diff --git a/frontend/src/tests/convert/ConvertIntegration.test.tsx b/frontend/src/tests/convert/ConvertIntegration.test.tsx index 4e6b7cc68c..c966615f44 100644 --- a/frontend/src/tests/convert/ConvertIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertIntegration.test.tsx @@ -1,6 +1,6 @@ /** * Integration tests for Convert Tool - Tests actual conversion functionality - * + * * These tests verify the integration between frontend components and backend: * 1. useConvertOperation hook makes correct API calls * 2. File upload/download flow functions properly @@ -10,7 +10,7 @@ */ import React from 'react'; -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation'; import { ConvertParameters } from '../../hooks/tools/convert/useConvertParameters'; @@ -28,8 +28,8 @@ vi.mock('../../services/fileStorage', () => ({ fileStorage: { init: vi.fn().mockResolvedValue(undefined), storeFile: vi.fn().mockImplementation((file, thumbnail) => { - return Promise.resolve({ - id: `mock-id-${file.name}`, + return Promise.resolve({ + id: `mock-id-${file.name}`, name: file.name, size: file.size, type: file.type, @@ -70,7 +70,7 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( ); describe('Convert Tool Integration Tests', () => { - + beforeEach(() => { vi.clearAllMocks(); // Setup default axios mock @@ -82,10 +82,10 @@ describe('Convert Tool Integration Tests', () => { }); describe('useConvertOperation Integration', () => { - + test('should make correct API call for PDF to PNG conversion', async () => { const mockBlob = new Blob(['fake-image-data'], { type: 'image/png' }); - mockedAxios.post.mockResolvedValueOnce({ + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob, status: 200, statusText: 'OK' @@ -108,7 +108,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -123,7 +135,7 @@ describe('Convert Tool Integration Tests', () => { ); // Verify FormData contains correct parameters - const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData; + const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formDataCall.get('imageFormat')).toBe('png'); expect(formDataCall.get('colorType')).toBe('color'); expect(formDataCall.get('dpi')).toBe('300'); @@ -138,7 +150,7 @@ describe('Convert Tool Integration Tests', () => { test('should handle API error responses correctly', async () => { const errorMessage = 'Invalid file format'; - mockedAxios.post.mockRejectedValueOnce({ + (mockedAxios.post as Mock).mockRejectedValueOnce({ response: { status: 400, data: errorMessage @@ -163,7 +175,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -177,7 +201,7 @@ describe('Convert Tool Integration Tests', () => { }); test('should handle network errors gracefully', async () => { - mockedAxios.post.mockRejectedValueOnce(new Error('Network error')); + (mockedAxios.post as Mock).mockRejectedValueOnce(new Error('Network error')); const { result } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper @@ -196,7 +220,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -209,10 +245,10 @@ describe('Convert Tool Integration Tests', () => { }); describe('API and Hook Integration', () => { - + test('should correctly map image conversion parameters to API call', async () => { const mockBlob = new Blob(['fake-data'], { type: 'image/jpeg' }); - mockedAxios.post.mockResolvedValueOnce({ + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob, status: 200, headers: { @@ -229,7 +265,6 @@ describe('Convert Tool Integration Tests', () => { const parameters: ConvertParameters = { fromExtension: 'pdf', toExtension: 'jpg', - pageNumbers: 'all', imageOptions: { colorType: 'grayscale', dpi: 150, @@ -239,7 +274,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -247,12 +294,12 @@ describe('Convert Tool Integration Tests', () => { }); // Verify integration: hook parameters → FormData → axios call → hook state - const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData; + const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formDataCall.get('imageFormat')).toBe('jpg'); expect(formDataCall.get('colorType')).toBe('grayscale'); expect(formDataCall.get('dpi')).toBe('150'); expect(formDataCall.get('singleOrMultiple')).toBe('single'); - + // Verify complete workflow: API response → hook state → FileContext integration expect(result.current.downloadUrl).toBeTruthy(); expect(result.current.files).toHaveLength(1); @@ -262,7 +309,7 @@ describe('Convert Tool Integration Tests', () => { test('should make correct API call for PDF to CSV conversion with simplified workflow', async () => { const mockBlob = new Blob(['fake-csv-data'], { type: 'text/csv' }); - mockedAxios.post.mockResolvedValueOnce({ + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob, status: 200, statusText: 'OK' @@ -285,7 +332,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -300,7 +359,7 @@ describe('Convert Tool Integration Tests', () => { ); // Verify FormData contains correct parameters for simplified CSV conversion - const formDataCall = mockedAxios.post.mock.calls[0][1] as FormData; + const formDataCall = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formDataCall.get('pageNumbers')).toBe('all'); // Always "all" for simplified workflow expect(formDataCall.get('fileInput')).toBe(testFile); @@ -329,7 +388,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -345,10 +416,10 @@ describe('Convert Tool Integration Tests', () => { }); describe('File Upload Integration', () => { - + test('should handle multiple file uploads correctly', async () => { const mockBlob = new Blob(['zip-content'], { type: 'application/zip' }); - mockedAxios.post.mockResolvedValueOnce({ data: mockBlob }); + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob }); const { result } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper @@ -369,7 +440,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -377,14 +460,14 @@ describe('Convert Tool Integration Tests', () => { }); // Verify both files were uploaded - const calls = mockedAxios.post.mock.calls; + const calls = (mockedAxios.post as Mock).mock.calls; for (let i = 0; i < calls.length; i++) { const formData = calls[i][1] as FormData; const fileInputs = formData.getAll('fileInput'); expect(fileInputs).toHaveLength(1); expect(fileInputs[0]).toBeInstanceOf(File); - expect(fileInputs[0].name).toBe(files[i].name); + expect((fileInputs[0] as File).name).toBe(files[i].name); } }); @@ -406,7 +489,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -419,9 +514,9 @@ describe('Convert Tool Integration Tests', () => { }); describe('Error Boundary Integration', () => { - + test('should handle corrupted file gracefully', async () => { - mockedAxios.post.mockRejectedValueOnce({ + (mockedAxios.post as Mock).mockRejectedValueOnce({ response: { status: 422, data: 'Processing failed' @@ -445,7 +540,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -457,7 +564,7 @@ describe('Convert Tool Integration Tests', () => { }); test('should handle backend service unavailable', async () => { - mockedAxios.post.mockRejectedValueOnce({ + (mockedAxios.post as Mock).mockRejectedValueOnce({ response: { status: 503, data: 'Service unavailable' @@ -481,7 +588,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -494,10 +613,10 @@ describe('Convert Tool Integration Tests', () => { }); describe('FileContext Integration', () => { - + test('should record operation in FileContext', async () => { const mockBlob = new Blob(['fake-data'], { type: 'image/png' }); - mockedAxios.post.mockResolvedValueOnce({ + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob, status: 200, headers: { @@ -523,7 +642,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -538,7 +669,7 @@ describe('Convert Tool Integration Tests', () => { test('should clean up blob URLs on reset', async () => { const mockBlob = new Blob(['fake-data'], { type: 'image/png' }); - mockedAxios.post.mockResolvedValueOnce({ + (mockedAxios.post as Mock).mockResolvedValueOnce({ data: mockBlob, status: 200, headers: { @@ -564,7 +695,19 @@ describe('Convert Tool Integration Tests', () => { combineImages: true }, isSmartDetection: false, - smartDetectionType: 'none' + smartDetectionType: 'none', + htmlOptions: { + zoomLevel: 0 + }, + emailOptions: { + includeAttachments: false, + maxAttachmentSizeMB: 0, + downloadHtml: false, + includeAllRecipients: false + }, + pdfaOptions: { + outputFormat: '' + } }; await act(async () => { @@ -586,35 +729,35 @@ describe('Convert Tool Integration Tests', () => { /** * Additional Integration Tests That Require Real Backend - * + * * These tests would require a running backend server and are better suited * for E2E testing with tools like Playwright or Cypress: - * + * * 1. **Real File Conversion Tests** * - Upload actual PDF files and verify conversion quality * - Test image format outputs are valid and viewable * - Test CSV/TXT outputs contain expected content * - Test file size limits and memory constraints - * + * * 2. **Performance Integration Tests** * - Test conversion time for various file sizes * - Test memory usage during large file conversions * - Test concurrent conversion requests * - Test timeout handling for long-running conversions - * + * * 3. **Authentication Integration** * - Test conversions with and without authentication * - Test rate limiting and user quotas * - Test permission-based endpoint access - * + * * 4. **File Preview Integration** * - Test that converted files integrate correctly with viewer * - Test thumbnail generation for converted files * - Test file download functionality * - Test FileContext persistence across tool switches - * + * * 5. **Endpoint Availability Tests** * - Test real endpoint availability checking * - Test graceful degradation when endpoints are disabled * - Test dynamic endpoint configuration updates - */ \ No newline at end of file + */ diff --git a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx index 64aafc4888..764095ba8a 100644 --- a/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx +++ b/frontend/src/tests/convert/ConvertSmartDetectionIntegration.test.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, test, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; import { renderHook, act, waitFor } from '@testing-library/react'; import { useConvertOperation } from '../../hooks/tools/convert/useConvertOperation'; import { useConvertParameters } from '../../hooks/tools/convert/useConvertParameters'; @@ -54,12 +54,12 @@ const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( ); describe('Convert Tool - Smart Detection Integration Tests', () => { - + beforeEach(() => { vi.clearAllMocks(); - + // Mock successful API response - mockedAxios.post.mockResolvedValue({ + (mockedAxios.post as Mock).mockResolvedValue({ data: new Blob(['fake converted content'], { type: 'application/pdf' }) }); }); @@ -74,25 +74,25 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Create mock DOCX file const docxFile = new File(['docx content'], 'document.docx', { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); - + // Test auto-detection act(() => { paramsResult.current.analyzeFileTypes([docxFile]); }); - + await waitFor(() => { expect(paramsResult.current.parameters.fromExtension).toBe('docx'); expect(paramsResult.current.parameters.toExtension).toBe('pdf'); expect(paramsResult.current.parameters.isSmartDetection).toBe(false); }); - + // Test conversion operation await act(async () => { await operationResult.current.executeOperation( @@ -100,7 +100,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { [docxFile] ); }); - + expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), { responseType: 'blob' }); @@ -110,25 +110,25 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Create mock unknown file const unknownFile = new File(['unknown content'], 'document.xyz', { type: 'application/octet-stream' }); - + // Test auto-detection act(() => { paramsResult.current.analyzeFileTypes([unknownFile]); }); - + await waitFor(() => { expect(paramsResult.current.parameters.fromExtension).toBe('file-xyz'); expect(paramsResult.current.parameters.toExtension).toBe('pdf'); // Fallback expect(paramsResult.current.parameters.isSmartDetection).toBe(false); }); - + // Test conversion operation await act(async () => { await operationResult.current.executeOperation( @@ -136,7 +136,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { [unknownFile] ); }); - + expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), { responseType: 'blob' }); @@ -144,35 +144,35 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); describe('Multi-File Smart Detection Flow', () => { - + test('should detect all images and use img-to-pdf endpoint', async () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Create mock image files const imageFiles = [ new File(['jpg content'], 'photo1.jpg', { type: 'image/jpeg' }), new File(['png content'], 'photo2.png', { type: 'image/png' }), new File(['gif content'], 'photo3.gif', { type: 'image/gif' }) ]; - + // Test smart detection for all images act(() => { paramsResult.current.analyzeFileTypes(imageFiles); }); - + await waitFor(() => { expect(paramsResult.current.parameters.fromExtension).toBe('image'); expect(paramsResult.current.parameters.toExtension).toBe('pdf'); expect(paramsResult.current.parameters.isSmartDetection).toBe(true); expect(paramsResult.current.parameters.smartDetectionType).toBe('images'); }); - + // Test conversion operation await act(async () => { await operationResult.current.executeOperation( @@ -180,13 +180,13 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { imageFiles ); }); - + expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/img/pdf', expect.any(FormData), { responseType: 'blob' }); - + // Should send all files in single request - const formData = mockedAxios.post.mock.calls[0][1] as FormData; + const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; const files = formData.getAll('fileInput'); expect(files).toHaveLength(3); }); @@ -195,30 +195,30 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Create mixed file types const mixedFiles = [ new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }), new File(['docx content'], 'spreadsheet.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), new File(['pptx content'], 'presentation.pptx', { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }) ]; - + // Test smart detection for mixed types act(() => { paramsResult.current.analyzeFileTypes(mixedFiles); }); - + await waitFor(() => { expect(paramsResult.current.parameters.fromExtension).toBe('any'); expect(paramsResult.current.parameters.toExtension).toBe('pdf'); expect(paramsResult.current.parameters.isSmartDetection).toBe(true); expect(paramsResult.current.parameters.smartDetectionType).toBe('mixed'); }); - + // Test conversion operation await act(async () => { await operationResult.current.executeOperation( @@ -226,7 +226,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { mixedFiles ); }); - + expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/file/pdf', expect.any(FormData), { responseType: 'blob' }); @@ -236,29 +236,29 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Create mock web files const webFiles = [ new File(['content'], 'page1.html', { type: 'text/html' }), new File(['zip content'], 'site.zip', { type: 'application/zip' }) ]; - + // Test smart detection for web files act(() => { paramsResult.current.analyzeFileTypes(webFiles); }); - + await waitFor(() => { expect(paramsResult.current.parameters.fromExtension).toBe('html'); expect(paramsResult.current.parameters.toExtension).toBe('pdf'); expect(paramsResult.current.parameters.isSmartDetection).toBe(true); expect(paramsResult.current.parameters.smartDetectionType).toBe('web'); }); - + // Test conversion operation await act(async () => { await operationResult.current.executeOperation( @@ -266,29 +266,29 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { webFiles ); }); - + expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/html/pdf', expect.any(FormData), { responseType: 'blob' }); - + // Should process files separately for web files expect(mockedAxios.post).toHaveBeenCalledTimes(2); }); }); describe('Web and Email Conversion Options Integration', () => { - + test('should send correct HTML parameters for web-to-pdf conversion', async () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + const htmlFile = new File(['content'], 'page.html', { type: 'text/html' }); - + // Set up HTML conversion parameters act(() => { paramsResult.current.analyzeFileTypes([htmlFile]); @@ -296,15 +296,15 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { zoomLevel: 1.5 }); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, [htmlFile] ); }); - - const formData = mockedAxios.post.mock.calls[0][1] as FormData; + + const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formData.get('zoom')).toBe('1.5'); }); @@ -312,13 +312,13 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + const emlFile = new File(['email content'], 'email.eml', { type: 'message/rfc822' }); - + // Set up email conversion parameters act(() => { paramsResult.current.updateParameter('fromExtension', 'eml'); @@ -330,15 +330,15 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { includeAllRecipients: true }); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, [emlFile] ); }); - - const formData = mockedAxios.post.mock.calls[0][1] as FormData; + + const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formData.get('includeAttachments')).toBe('false'); expect(formData.get('maxAttachmentSizeMB')).toBe('20'); expect(formData.get('downloadHtml')).toBe('true'); @@ -349,13 +349,13 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + const pdfFile = new File(['pdf content'], 'document.pdf', { type: 'application/pdf' }); - + // Set up PDF/A conversion parameters act(() => { paramsResult.current.updateParameter('fromExtension', 'pdf'); @@ -364,15 +364,15 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { outputFormat: 'pdfa' }); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, [pdfFile] ); }); - - const formData = mockedAxios.post.mock.calls[0][1] as FormData; + + const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formData.get('outputFormat')).toBe('pdfa'); expect(mockedAxios.post).toHaveBeenCalledWith('/api/v1/convert/pdf/pdfa', expect.any(FormData), { responseType: 'blob' @@ -381,21 +381,21 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); describe('Image Conversion Options Integration', () => { - + test('should send correct parameters for image-to-pdf conversion', async () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + const imageFiles = [ new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) ]; - + // Set up image conversion parameters act(() => { paramsResult.current.analyzeFileTypes(imageFiles); @@ -408,15 +408,15 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { combineImages: true }); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, imageFiles ); }); - - const formData = mockedAxios.post.mock.calls[0][1] as FormData; + + const formData = (mockedAxios.post as Mock).mock.calls[0][1] as FormData; expect(formData.get('fitOption')).toBe('fitToPage'); expect(formData.get('colorType')).toBe('grayscale'); expect(formData.get('autoRotate')).toBe('false'); @@ -426,16 +426,16 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + const imageFiles = [ new File(['jpg1'], 'photo1.jpg', { type: 'image/jpeg' }), new File(['jpg2'], 'photo2.jpg', { type: 'image/jpeg' }) ]; - + // Set up for separate processing act(() => { paramsResult.current.analyzeFileTypes(imageFiles); @@ -444,55 +444,55 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { combineImages: false }); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, imageFiles ); }); - + // Should make separate API calls for each file expect(mockedAxios.post).toHaveBeenCalledTimes(2); }); }); describe('Error Scenarios in Smart Detection', () => { - + test('should handle partial failures in multi-file processing', async () => { const { result: paramsResult } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const { result: operationResult } = renderHook(() => useConvertOperation(), { wrapper: TestWrapper }); - + // Mock one success, one failure - mockedAxios.post + (mockedAxios.post as Mock) .mockResolvedValueOnce({ data: new Blob(['converted1'], { type: 'application/pdf' }) }) .mockRejectedValueOnce(new Error('File 2 failed')); - + const mixedFiles = [ new File(['file1'], 'doc1.txt', { type: 'text/plain' }), new File(['file2'], 'doc2.xyz', { type: 'application/octet-stream' }) ]; - + // Set up for separate processing (mixed smart detection) act(() => { paramsResult.current.analyzeFileTypes(mixedFiles); }); - + await act(async () => { await operationResult.current.executeOperation( paramsResult.current.parameters, mixedFiles ); }); - + await waitFor(() => { // Should have processed at least one file successfully expect(operationResult.current.files.length).toBeGreaterThan(0); @@ -502,12 +502,12 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { }); describe('Real File Extension Detection', () => { - + test('should correctly detect various file extensions', async () => { const { result } = renderHook(() => useConvertParameters(), { wrapper: TestWrapper }); - + const testCases = [ { filename: 'document.PDF', expected: 'pdf' }, { filename: 'image.JPEG', expected: 'jpg' }, // JPEG should normalize to jpg @@ -517,11 +517,11 @@ describe('Convert Tool - Smart Detection Integration Tests', () => { { filename: '.hidden', expected: 'hidden' }, { filename: 'noextension', expected: '' } ]; - + testCases.forEach(({ filename, expected }) => { const detected = detectFileExtension(filename); expect(detected).toBe(expected); }); }); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/theme/mantineTheme.ts b/frontend/src/theme/mantineTheme.ts index cc263f126e..27e70d461f 100644 --- a/frontend/src/theme/mantineTheme.ts +++ b/frontend/src/theme/mantineTheme.ts @@ -64,14 +64,6 @@ export const mantineTheme = createTheme({ xl: 'var(--shadow-xl)', }, - // Font weights - fontWeights: { - normal: 'var(--font-weight-normal)', - medium: 'var(--font-weight-medium)', - semibold: 'var(--font-weight-semibold)', - bold: 'var(--font-weight-bold)', - }, - // Component customizations components: { Button: { @@ -83,7 +75,7 @@ export const mantineTheme = createTheme({ }, variants: { // Custom button variant for PDF tools - pdfTool: (theme) => ({ + pdfTool: (theme: any) => ({ root: { backgroundColor: 'var(--bg-surface)', border: '1px solid var(--border-default)', @@ -95,7 +87,7 @@ export const mantineTheme = createTheme({ }, }), }, - }, + } as any, Paper: { styles: { @@ -287,28 +279,4 @@ export const mantineTheme = createTheme({ }, }, }, - - // Global styles - globalStyles: () => ({ - // Ensure smooth color transitions - '*': { - transition: 'background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease', - }, - - // Custom scrollbar styling - '*::-webkit-scrollbar': { - width: '8px', - height: '8px', - }, - '*::-webkit-scrollbar-track': { - backgroundColor: 'var(--bg-muted)', - }, - '*::-webkit-scrollbar-thumb': { - backgroundColor: 'var(--border-strong)', - borderRadius: 'var(--radius-md)', - }, - '*::-webkit-scrollbar-thumb:hover': { - backgroundColor: 'var(--color-primary-500)', - }, - }), }); diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index d9c049ae7e..6ced5a35ce 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -5,7 +5,11 @@ import { ProcessedFile } from './processing'; import { PDFDocument, PDFPage, PageOperation } from './pageEditor'; -export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr'; +export type ModeType = 'viewer' | 'pageEditor' | 'fileEditor' | 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize'; + +export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; + +export type ToolType = 'merge' | 'split' | 'compress' | 'ocr' | 'convert' | 'sanitize'; export type OperationType = 'merge' | 'split' | 'compress' | 'add' | 'remove' | 'replace' | 'convert' | 'upload' | 'ocr'; @@ -51,25 +55,27 @@ export interface FileContextState { // Core file management activeFiles: File[]; processedFiles: Map; - + // Current navigation state currentMode: ModeType; - + currentView: ViewType; + currentTool: ToolType | null; + // Edit history and state fileEditHistory: Map; globalFileOperations: FileOperation[]; // New comprehensive operation history fileOperationHistory: Map; - + // UI state that persists across views selectedFileIds: string[]; selectedPageNumbers: number[]; viewerConfig: ViewerConfig; - + // Processing state isProcessing: boolean; processingProgress: number; - + // Export state lastExportConfig?: { filename: string; @@ -85,24 +91,26 @@ export interface FileContextState { export interface FileContextActions { // File management - addFiles: (files: File[]) => Promise; + addFiles: (files: File[]) => Promise; removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void; replaceFile: (oldFileId: string, newFile: File) => Promise; clearAllFiles: () => void; - + // Navigation setCurrentMode: (mode: ModeType) => void; + setCurrentView: (view: ViewType) => void; + setCurrentTool: (tool: ToolType) => void; // Selection management setSelectedFiles: (fileIds: string[]) => void; setSelectedPages: (pageNumbers: number[]) => void; updateProcessedFile: (file: File, processedFile: ProcessedFile) => void; clearSelections: () => void; - + // Edit operations applyPageOperations: (fileId: string, operations: PageOperation[]) => void; applyFileOperation: (operation: FileOperation) => void; undoLastOperation: (fileId?: string) => void; - + // Operation history management recordOperation: (fileId: string, operation: FileOperation | PageOperation) => void; markOperationApplied: (fileId: string, operationId: string) => void; @@ -110,31 +118,31 @@ export interface FileContextActions { getFileHistory: (fileId: string) => FileOperationHistory | undefined; getAppliedOperations: (fileId: string) => (FileOperation | PageOperation)[]; clearFileHistory: (fileId: string) => void; - + // Viewer state updateViewerConfig: (config: Partial) => void; - + // Export configuration setExportConfig: (config: FileContextState['lastExportConfig']) => void; - - + + // Utility getFileById: (fileId: string) => File | undefined; getProcessedFileById: (fileId: string) => ProcessedFile | undefined; getCurrentFile: () => File | undefined; getCurrentProcessedFile: () => ProcessedFile | undefined; - + // Context persistence saveContext: () => Promise; loadContext: () => Promise; resetContext: () => void; - + // Navigation guard system setHasUnsavedChanges: (hasChanges: boolean) => void; requestNavigation: (navigationFn: () => void) => boolean; confirmNavigation: () => void; cancelNavigation: () => void; - + // Memory management trackBlobUrl: (url: string) => void; trackPdfDocument: (fileId: string, pdfDoc: any) => void; @@ -163,4 +171,4 @@ export interface FileContextUrlParams { pageIds?: string[]; zoom?: number; page?: number; -} \ No newline at end of file +} diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts index f5529aee30..6ad4318f79 100644 --- a/frontend/src/types/pageEditor.ts +++ b/frontend/src/types/pageEditor.ts @@ -13,6 +13,7 @@ export interface PDFDocument { file: File; pages: PDFPage[]; totalPages: number; + destroy?: () => void; } export interface PageOperation { @@ -43,7 +44,7 @@ export interface PageEditorFunctions { handleRedo: () => void; canUndo: boolean; canRedo: boolean; - handleRotate: () => void; + handleRotate: (direction: 'left' | 'right') => void; handleDelete: () => void; handleSplit: () => void; onExportSelected: () => void; diff --git a/frontend/src/types/tool.ts b/frontend/src/types/tool.ts index 731f0b90e2..9a27104000 100644 --- a/frontend/src/types/tool.ts +++ b/frontend/src/types/tool.ts @@ -35,6 +35,11 @@ export interface ToolResult { metadata?: Record; } +export interface ToolConfiguration { + maxFiles: number; + supportedFormats?: string[]; +} + export interface Tool { id: string; name: string; @@ -70,4 +75,4 @@ export interface FileSelectionComputed { isMultiFileMode: boolean; } -export interface FileSelectionContextValue extends FileSelectionState, FileSelectionActions, FileSelectionComputed {} \ No newline at end of file +export interface FileSelectionContextValue extends FileSelectionState, FileSelectionActions, FileSelectionComputed {} diff --git a/frontend/src/utils/fileUtils.ts b/frontend/src/utils/fileUtils.ts index 682cd9f3c3..040fc14c7f 100644 --- a/frontend/src/utils/fileUtils.ts +++ b/frontend/src/utils/fileUtils.ts @@ -49,12 +49,16 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?: size: storedFile.size, type: storedFile.type, lastModified: storedFile.lastModified, + webkitRelativePath: '', // Lazy-loading File interface methods arrayBuffer: async () => { const data = await fileStorage.getFileData(storedFile.id); if (!data) throw new Error(`File ${storedFile.name} not found in IndexedDB - may have been purged`); return data; }, + bytes: async () => { + return new Uint8Array(); + }, slice: (start?: number, end?: number, contentType?: string) => { // Return a promise-based slice that loads from IndexedDB return new Blob([], { type: contentType || storedFile.type }); @@ -66,9 +70,9 @@ export function createEnhancedFileFromStored(storedFile: StoredFile, thumbnail?: const data = await fileStorage.getFileData(storedFile.id); if (!data) throw new Error(`File ${storedFile.name} not found in IndexedDB - may have been purged`); return new TextDecoder().decode(data); - } + }, } as FileWithUrl; - + return enhancedFile; } @@ -79,28 +83,28 @@ export async function loadFilesFromIndexedDB(): Promise { try { await fileStorage.init(); const storedFiles = await fileStorage.getAllFileMetadata(); - + if (storedFiles.length === 0) { return []; } - + const restoredFiles: FileWithUrl[] = storedFiles .filter(storedFile => { // Filter out corrupted entries - return storedFile && - storedFile.name && + return storedFile && + storedFile.name && typeof storedFile.size === 'number'; }) .map(storedFile => { try { - return createEnhancedFileFromStored(storedFile); + return createEnhancedFileFromStored(storedFile as any); } catch (error) { console.error('Failed to restore file:', storedFile?.name || 'unknown', error); return null; } }) .filter((file): file is FileWithUrl => file !== null); - + return restoredFiles; } catch (error) { console.error('Failed to load files from IndexedDB:', error); @@ -134,17 +138,17 @@ export function shouldUseDirectIndexedDBAccess(file: FileWithUrl): boolean { */ export function detectFileExtension(filename: string): string { if (!filename || typeof filename !== 'string') return ''; - + const parts = filename.split('.'); // If there's no extension (no dots or only one part), return empty string if (parts.length <= 1) return ''; - + // Get the last part (extension) in lowercase let extension = parts[parts.length - 1].toLowerCase(); - + // Normalize common extension variants if (extension === 'jpeg') extension = 'jpg'; - + return extension; } @@ -155,10 +159,10 @@ export function detectFileExtension(filename: string): string { */ export function getFilenameWithoutExtension(filename: string): string { if (!filename || typeof filename !== 'string') return ''; - + const parts = filename.split('.'); if (parts.length <= 1) return filename; - + // Return all parts except the last one (extension) return parts.slice(0, -1).join('.'); } @@ -172,4 +176,4 @@ export function getFilenameWithoutExtension(filename: string): string { export function changeFileExtension(filename: string, newExtension: string): string { const nameWithoutExt = getFilenameWithoutExtension(filename); return `${nameWithoutExt}.${newExtension}`; -} \ No newline at end of file +} diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts index f4f224044b..6f69279d5a 100644 --- a/frontend/src/utils/thumbnailUtils.ts +++ b/frontend/src/utils/thumbnailUtils.ts @@ -6,9 +6,9 @@ import { getDocument } from "pdfjs-dist"; */ export function calculateScaleFromFileSize(fileSize: number): number { const MB = 1024 * 1024; - + if (fileSize < 1 * MB) return 0.6; // < 1MB: High quality - if (fileSize < 5 * MB) return 0.4; // 1-5MB: Medium-high quality + if (fileSize < 5 * MB) return 0.4; // 1-5MB: Medium-high quality if (fileSize < 15 * MB) return 0.3; // 5-15MB: Medium quality if (fileSize < 30 * MB) return 0.2; // 15-30MB: Low-medium quality return 0.15; // 30MB+: Low quality @@ -182,43 +182,41 @@ export async function generateThumbnailForFile(file: File): Promise( parameters: params, fileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0) } - }; + } as any /* FIX ME*/; return { operation, operationId, fileId }; -}; \ No newline at end of file +}; 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 73/77] 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 2d28b669c3..2642404cb1 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 74/77] 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 2642404cb1..453c4f4453 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 From 768ece6921e288c03c1292463032476e9f31a60b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 11 Aug 2025 12:27:18 +0100 Subject: [PATCH 75/77] Remove backend UI (#4023) # 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> --- app/common/build.gradle | 2 +- .../common/configuration/AppConfig.java | 5 +- .../FileFallbackTemplateResolver.java | 16 +- .../model/InputStreamTemplateResource.java | 8 +- .../software/SPDF/SPDFApplication.java | 62 +-- .../stirling/software/SPDF/UI/WebBrowser.java | 7 - .../software/SPDF/UI/impl/DesktopBrowser.java | 497 ------------------ .../software/SPDF/UI/impl/LoadingWindow.java | 351 ------------- .../SPDF/config/CleanUrlInterceptor.java | 8 +- .../software/SPDF/config/OpenApiConfig.java | 7 +- .../software/SPDF/config/WebMvcConfig.java | 19 +- .../controller/api/AnalysisController.java | 3 +- .../SPDF/controller/api/CropController.java | 4 +- .../api/EditTableOfContentsController.java | 4 +- .../SPDF/controller/api/MergeController.java | 4 +- .../api/MultiPageLayoutController.java | 4 +- .../api/PdfImageRemovalController.java | 4 +- .../controller/api/PdfOverlayController.java | 4 +- .../api/RearrangePagesPDFController.java | 4 +- .../controller/api/RotationController.java | 4 +- .../controller/api/ScalePagesController.java | 4 +- .../controller/api/SettingsController.java | 4 +- .../controller/api/SplitPDFController.java | 4 +- .../api/SplitPdfByChaptersController.java | 4 +- .../api/SplitPdfBySectionsController.java | 4 +- .../api/SplitPdfBySizeController.java | 4 +- .../api/ToSinglePageController.java | 4 +- .../SPDF/controller/api/UIDataController.java | 1 - .../api/converters/ConvertEmlToPDF.java | 4 +- .../api/converters/ConvertHtmlToPDF.java | 4 +- .../converters/ConvertImgPDFController.java | 4 +- .../api/converters/ConvertMarkdownToPdf.java | 4 +- .../converters/ConvertOfficeController.java | 4 +- .../api/converters/ConvertPDFToHtml.java | 4 +- .../api/converters/ConvertPDFToOffice.java | 4 +- .../api/converters/ConvertPDFToPDFA.java | 4 +- .../api/converters/ConvertWebsiteToPDF.java | 4 +- .../api/converters/ExtractCSVController.java | 4 +- .../api/filters/FilterController.java | 4 +- .../api/misc/AttachmentController.java | 4 +- .../api/misc/AutoRenameController.java | 4 +- .../api/misc/AutoSplitPdfController.java | 4 +- .../api/misc/BlankPageController.java | 4 +- .../api/misc/CompressController.java | 4 +- .../controller/api/misc/ConfigController.java | 3 +- .../api/misc/DecompressPdfController.java | 4 +- .../api/misc/ExtractImageScansController.java | 4 +- .../api/misc/ExtractImagesController.java | 4 +- .../api/misc/FlattenController.java | 4 +- .../api/misc/MetadataController.java | 3 +- .../controller/api/misc/OCRController.java | 4 +- .../api/misc/OverlayImageController.java | 4 +- .../api/misc/PageNumbersController.java | 4 +- .../api/misc/PrintFileController.java | 2 - .../controller/api/misc/RepairController.java | 4 +- .../misc/ReplaceAndInvertColorController.java | 4 +- .../api/misc/ScannerEffectController.java | 4 +- .../controller/api/misc/ShowJavascript.java | 4 +- .../controller/api/misc/StampController.java | 4 +- .../api/misc/UnlockPDFFormsController.java | 4 +- .../api/pipeline/PipelineController.java | 4 +- .../api/security/CertSignController.java | 4 +- .../controller/api/security/GetInfoOnPDF.java | 4 +- .../api/security/PasswordController.java | 4 +- .../api/security/RedactController.java | 4 +- .../security/RemoveCertSignController.java | 4 +- .../api/security/SanitizeController.java | 4 +- .../security/ValidateSignatureController.java | 8 +- .../api/security/WatermarkController.java | 4 +- .../web/ConverterWebController.java | 49 +- .../controller/web/GeneralWebController.java | 67 ++- .../controller/web/HomeWebController.java | 21 +- .../controller/web/OtherWebController.java | 70 ++- .../web/ReactRoutingController.java | 3 +- .../controller/web/SecurityWebController.java | 37 +- .../controller/web/SignatureController.java | 3 +- .../api/converters/ConvertPDFToMarkdown.java | 4 +- .../SPDF/service/SignatureService.java | 4 +- .../src/main/resources/application.properties | 10 +- app/proprietary/build.gradle | 4 +- .../controller/AuditDashboardController.java | 3 +- .../security/config/AccountWebController.java | 23 +- .../controller/api/EmailController.java | 4 +- .../controller/web/DatabaseWebController.java | 7 +- .../controller/web/TeamWebController.java | 10 +- testing/compose/docker-compose-ultra-lite.yml | 2 +- testing/test.sh | 37 +- 87 files changed, 318 insertions(+), 1250 deletions(-) delete mode 100644 app/core/src/main/java/stirling/software/SPDF/UI/WebBrowser.java delete mode 100644 app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java delete mode 100644 app/core/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java diff --git a/app/common/build.gradle b/app/common/build.gradle index 39dab8ded9..f79ec69829 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -29,7 +29,7 @@ spotless { dependencies { api 'org.springframework.boot:spring-boot-starter-web' api 'org.springframework.boot:spring-boot-starter-aop' - api 'org.springframework.boot:spring-boot-starter-thymeleaf' + // api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1' api 'com.fathzer:javaluator:3.0.6' api 'com.posthog.java:posthog:1.2.0' diff --git a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java index f611f42ca9..170261ff81 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java +++ b/app/common/src/main/java/stirling/software/common/configuration/AppConfig.java @@ -10,7 +10,6 @@ import java.util.Properties; import java.util.function.Predicate; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; @@ -19,9 +18,7 @@ import org.springframework.context.annotation.Scope; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; import org.springframework.util.ClassUtils; -import org.thymeleaf.spring6.SpringTemplateEngine; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -51,6 +48,7 @@ public class AppConfig { @Value("${server.port:8080}") private String serverPort; + /* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete @Bean @ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true") public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) { @@ -58,6 +56,7 @@ public class AppConfig { templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader)); return templateEngine; } + */ @Bean(name = "loginEnabled") public boolean loginEnabled() { diff --git a/app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java b/app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java index 320d9aaac9..9c5e0a7ae7 100644 --- a/app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java +++ b/app/common/src/main/java/stirling/software/common/configuration/FileFallbackTemplateResolver.java @@ -1,19 +1,8 @@ package stirling.software.common.configuration; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; +/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed + * This class will be removed when frontend migration to React is complete -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.thymeleaf.IEngineConfiguration; -import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver; -import org.thymeleaf.templateresource.FileTemplateResource; -import org.thymeleaf.templateresource.ITemplateResource; - -import lombok.extern.slf4j.Slf4j; - -import stirling.software.common.model.InputStreamTemplateResource; @Slf4j public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver { @@ -57,3 +46,4 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe return null; } } +*/ diff --git a/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java b/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java index 4bc81cafa0..3bac16fef7 100644 --- a/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java +++ b/app/common/src/main/java/stirling/software/common/model/InputStreamTemplateResource.java @@ -1,11 +1,8 @@ package stirling.software.common.model; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed + * This class will be removed when frontend migration to React is complete -import org.thymeleaf.templateresource.ITemplateResource; public class InputStreamTemplateResource implements ITemplateResource { private InputStream inputStream; @@ -42,3 +39,4 @@ public class InputStreamTemplateResource implements ITemplateResource { return inputStream != null; } } +*/ diff --git a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java index 2131b42398..288d11a8d6 100644 --- a/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/app/core/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -10,7 +10,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Properties; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; @@ -23,7 +22,6 @@ import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.UI.WebBrowser; import stirling.software.common.configuration.AppConfig; import stirling.software.common.configuration.ConfigInitializer; import stirling.software.common.configuration.InstallationPathConfig; @@ -47,17 +45,15 @@ public class SPDFApplication { private final AppConfig appConfig; private final Environment env; private final ApplicationProperties applicationProperties; - private final WebBrowser webBrowser; + + // private final WebBrowser webBrowser; // Removed - desktop UI eliminated public SPDFApplication( - AppConfig appConfig, - Environment env, - ApplicationProperties applicationProperties, - @Autowired(required = false) WebBrowser webBrowser) { + AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) { this.appConfig = appConfig; this.env = env; this.applicationProperties = applicationProperties; - this.webBrowser = webBrowser; + // this.webBrowser = webBrowser; // Removed - desktop UI eliminated } public static void main(String[] args) throws IOException, InterruptedException { @@ -151,28 +147,31 @@ public class SPDFApplication { serverPortStatic = serverPort; String url = baseUrl + ":" + getStaticPort() + contextPath; - if (webBrowser != null - && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { - webBrowser.initWebUI(url); - } else { - String browserOpenEnv = env.getProperty("BROWSER_OPEN"); - boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); - if (browserOpen) { - try { - String os = System.getProperty("os.name").toLowerCase(); - Runtime rt = Runtime.getRuntime(); + // Desktop UI initialization removed - webBrowser dependency eliminated + // Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property + if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { + log.info("Desktop UI mode enabled, but WebBrowser functionality has been removed"); + // webBrowser.initWebUI(url); // Removed - desktop UI eliminated + } - if (os.contains("win")) { - // For Windows - SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); - } else if (os.contains("mac")) { - SystemCommand.runCommand(rt, "open " + url); - } else if (os.contains("nix") || os.contains("nux")) { - SystemCommand.runCommand(rt, "xdg-open " + url); - } - } catch (IOException e) { - log.error("Error opening browser: {}", e.getMessage()); + // Standard browser opening logic + String browserOpenEnv = env.getProperty("BROWSER_OPEN"); + boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); + if (browserOpen) { + try { + String os = System.getProperty("os.name").toLowerCase(); + Runtime rt = Runtime.getRuntime(); + + if (os.contains("win")) { + // For Windows + SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url); + } else if (os.contains("mac")) { + SystemCommand.runCommand(rt, "open " + url); + } else if (os.contains("nix") || os.contains("nux")) { + SystemCommand.runCommand(rt, "xdg-open " + url); } + } catch (IOException e) { + log.error("Error opening browser: {}", e.getMessage()); } } } @@ -189,9 +188,10 @@ public class SPDFApplication { @PreDestroy public void cleanup() { - if (webBrowser != null) { - webBrowser.cleanup(); - } + // webBrowser cleanup removed - desktop UI eliminated + // if (webBrowser != null) { + // webBrowser.cleanup(); + // } } private static void printStartupLogs() { diff --git a/app/core/src/main/java/stirling/software/SPDF/UI/WebBrowser.java b/app/core/src/main/java/stirling/software/SPDF/UI/WebBrowser.java deleted file mode 100644 index b884888fe1..0000000000 --- a/app/core/src/main/java/stirling/software/SPDF/UI/WebBrowser.java +++ /dev/null @@ -1,7 +0,0 @@ -package stirling.software.SPDF.UI; - -public interface WebBrowser { - void initWebUI(String url); - - void cleanup(); -} diff --git a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java b/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java deleted file mode 100644 index 959e7f354c..0000000000 --- a/app/core/src/main/java/stirling/software/SPDF/UI/impl/DesktopBrowser.java +++ /dev/null @@ -1,497 +0,0 @@ -package stirling.software.SPDF.UI.impl; - -import java.awt.AWTException; -import java.awt.BorderLayout; -import java.awt.Frame; -import java.awt.Image; -import java.awt.MenuItem; -import java.awt.PopupMenu; -import java.awt.SystemTray; -import java.awt.TrayIcon; -import java.awt.event.WindowEvent; -import java.awt.event.WindowStateListener; -import java.io.File; -import java.io.InputStream; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; - -import javax.imageio.ImageIO; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; -import javax.swing.Timer; - -import org.cef.CefApp; -import org.cef.CefClient; -import org.cef.CefSettings; -import org.cef.browser.CefBrowser; -import org.cef.callback.CefBeforeDownloadCallback; -import org.cef.callback.CefDownloadItem; -import org.cef.callback.CefDownloadItemCallback; -import org.cef.handler.CefDownloadHandlerAdapter; -import org.cef.handler.CefLoadHandlerAdapter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PreDestroy; - -import lombok.extern.slf4j.Slf4j; - -import me.friwi.jcefmaven.CefAppBuilder; -import me.friwi.jcefmaven.EnumProgress; -import me.friwi.jcefmaven.MavenCefAppHandlerAdapter; -import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; - -import stirling.software.SPDF.UI.WebBrowser; -import stirling.software.common.configuration.InstallationPathConfig; -import stirling.software.common.util.UIScaling; - -@Component -@Slf4j -@ConditionalOnProperty( - name = "STIRLING_PDF_DESKTOP_UI", - havingValue = "true", - matchIfMissing = false) -public class DesktopBrowser implements WebBrowser { - private static CefApp cefApp; - private static CefClient client; - private static CefBrowser browser; - private static JFrame frame; - private static LoadingWindow loadingWindow; - private static volatile boolean browserInitialized = false; - private static TrayIcon trayIcon; - private static SystemTray systemTray; - - public DesktopBrowser() { - SwingUtilities.invokeLater( - () -> { - loadingWindow = new LoadingWindow(null, "Initializing..."); - loadingWindow.setVisible(true); - }); - } - - public void initWebUI(String url) { - CompletableFuture.runAsync( - () -> { - try { - CefAppBuilder builder = new CefAppBuilder(); - configureCefSettings(builder); - builder.setProgressHandler(createProgressHandler()); - builder.setInstallDir( - new File(InstallationPathConfig.getClientWebUIPath())); - // Build and initialize CEF - cefApp = builder.build(); - client = cefApp.createClient(); - - // Set up download handler - setupDownloadHandler(); - - // Create browser and frame on EDT - SwingUtilities.invokeAndWait( - () -> { - browser = client.createBrowser(url, false, false); - setupMainFrame(); - setupLoadHandler(); - - // Force initialize UI after 7 seconds if not already done - Timer timeoutTimer = - new Timer( - 2500, - e -> { - log.warn( - "Loading timeout reached. Forcing" - + " UI transition."); - if (!browserInitialized) { - // Force UI initialization - forceInitializeUI(); - } - }); - timeoutTimer.setRepeats(false); - timeoutTimer.start(); - }); - } catch (Exception e) { - log.error("Error initializing JCEF browser: ", e); - cleanup(); - } - }); - } - - private void configureCefSettings(CefAppBuilder builder) { - CefSettings settings = builder.getCefSettings(); - String basePath = InstallationPathConfig.getClientWebUIPath(); - log.info("basePath " + basePath); - settings.cache_path = new File(basePath + "cache").getAbsolutePath(); - settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath(); - // settings.browser_subprocess_path = new File(basePath + - // "subprocess").getAbsolutePath(); - // settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath(); - // settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath(); - settings.log_file = new File(basePath, "debug.log").getAbsolutePath(); - - settings.persist_session_cookies = true; - settings.windowless_rendering_enabled = false; - settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO; - - builder.setAppHandler( - new MavenCefAppHandlerAdapter() { - @Override - public void stateHasChanged(org.cef.CefApp.CefAppState state) { - log.info("CEF state changed: " + state); - if (state == CefApp.CefAppState.TERMINATED) { - System.exit(0); - } - } - }); - } - - private void setupDownloadHandler() { - client.addDownloadHandler( - new CefDownloadHandlerAdapter() { - @Override - public boolean onBeforeDownload( - CefBrowser browser, - CefDownloadItem downloadItem, - String suggestedName, - CefBeforeDownloadCallback callback) { - callback.Continue("", true); - return true; - } - - @Override - public void onDownloadUpdated( - CefBrowser browser, - CefDownloadItem downloadItem, - CefDownloadItemCallback callback) { - if (downloadItem.isComplete()) { - log.info("Download completed: " + downloadItem.getFullPath()); - } else if (downloadItem.isCanceled()) { - log.info("Download canceled: " + downloadItem.getFullPath()); - } - } - }); - } - - private ConsoleProgressHandler createProgressHandler() { - return new ConsoleProgressHandler() { - @Override - public void handleProgress(EnumProgress state, float percent) { - Objects.requireNonNull(state, "state cannot be null"); - SwingUtilities.invokeLater( - () -> { - if (loadingWindow != null) { - switch (state) { - case LOCATING: - loadingWindow.setStatus("Locating Files..."); - loadingWindow.setProgress(0); - break; - case DOWNLOADING: - if (percent >= 0) { - loadingWindow.setStatus( - String.format( - "Downloading additional files: %.0f%%", - percent)); - loadingWindow.setProgress((int) percent); - } - break; - case EXTRACTING: - loadingWindow.setStatus("Extracting files..."); - loadingWindow.setProgress(60); - break; - case INITIALIZING: - loadingWindow.setStatus("Initializing UI..."); - loadingWindow.setProgress(80); - break; - case INITIALIZED: - loadingWindow.setStatus("Finalising startup..."); - loadingWindow.setProgress(90); - break; - } - } - }); - } - }; - } - - private void setupMainFrame() { - frame = new JFrame("Stirling-PDF"); - frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - frame.setUndecorated(true); - frame.setOpacity(0.0f); - - JPanel contentPane = new JPanel(new BorderLayout()); - contentPane.setDoubleBuffered(true); - contentPane.add(browser.getUIComponent(), BorderLayout.CENTER); - frame.setContentPane(contentPane); - - frame.addWindowListener( - new java.awt.event.WindowAdapter() { - @Override - public void windowClosing(java.awt.event.WindowEvent windowEvent) { - cleanup(); - System.exit(0); - } - }); - - frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - - loadIcon(); - } - - private void setupLoadHandler() { - final long initStartTime = System.currentTimeMillis(); - log.info("Setting up load handler at: {}", initStartTime); - - client.addLoadHandler( - new CefLoadHandlerAdapter() { - @Override - public void onLoadingStateChange( - CefBrowser browser, - boolean isLoading, - boolean canGoBack, - boolean canGoForward) { - log.debug( - "Loading state change - isLoading: {}, canGoBack: {}, canGoForward:" - + " {}, browserInitialized: {}, Time elapsed: {}ms", - isLoading, - canGoBack, - canGoForward, - browserInitialized, - System.currentTimeMillis() - initStartTime); - - if (!isLoading && !browserInitialized) { - log.info( - "Browser finished loading, preparing to initialize UI" - + " components"); - browserInitialized = true; - SwingUtilities.invokeLater( - () -> { - try { - if (loadingWindow != null) { - log.info("Starting UI initialization sequence"); - - // Close loading window first - loadingWindow.setVisible(false); - loadingWindow.dispose(); - loadingWindow = null; - log.info("Loading window disposed"); - - // Then setup the main frame - frame.setVisible(false); - frame.dispose(); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.pack(); - frame.setSize( - UIScaling.scaleWidth(1280), - UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - log.debug("Frame reconfigured"); - - // Show the main frame - frame.setVisible(true); - frame.requestFocus(); - frame.toFront(); - log.info("Main frame displayed and focused"); - - // Focus the browser component - Timer focusTimer = - new Timer( - 100, - e -> { - try { - browser.getUIComponent() - .requestFocus(); - log.info( - "Browser component" - + " focused"); - } catch (Exception ex) { - log.error( - "Error focusing" - + " browser", - ex); - } - }); - focusTimer.setRepeats(false); - focusTimer.start(); - } - } catch (Exception e) { - log.error("Error during UI initialization", e); - // Attempt cleanup on error - if (loadingWindow != null) { - loadingWindow.dispose(); - loadingWindow = null; - } - if (frame != null) { - frame.setVisible(true); - frame.requestFocus(); - } - } - }); - } - } - }); - } - - private void setupTrayIcon(Image icon) { - if (!SystemTray.isSupported()) { - log.warn("System tray is not supported"); - return; - } - - try { - systemTray = SystemTray.getSystemTray(); - - // Create popup menu - PopupMenu popup = new PopupMenu(); - - // Create menu items - MenuItem showItem = new MenuItem("Show"); - showItem.addActionListener( - e -> { - frame.setVisible(true); - frame.setState(Frame.NORMAL); - }); - - MenuItem exitItem = new MenuItem("Exit"); - exitItem.addActionListener( - e -> { - cleanup(); - System.exit(0); - }); - - // Add menu items to popup menu - popup.add(showItem); - popup.addSeparator(); - popup.add(exitItem); - - // Create tray icon - trayIcon = new TrayIcon(icon, "Stirling-PDF", popup); - trayIcon.setImageAutoSize(true); - - // Add double-click behavior - trayIcon.addActionListener( - e -> { - frame.setVisible(true); - frame.setState(Frame.NORMAL); - }); - - // Add tray icon to system tray - systemTray.add(trayIcon); - - // Modify frame behavior to minimize to tray - frame.addWindowStateListener( - new WindowStateListener() { - public void windowStateChanged(WindowEvent e) { - if (e.getNewState() == Frame.ICONIFIED) { - frame.setVisible(false); - } - } - }); - - } catch (AWTException e) { - log.error("Error setting up system tray icon", e); - } - } - - private void loadIcon() { - try { - Image icon = null; - String[] iconPaths = {"/static/favicon.ico"}; - - for (String path : iconPaths) { - if (icon != null) break; - try { - try (InputStream is = getClass().getResourceAsStream(path)) { - if (is != null) { - icon = ImageIO.read(is); - break; - } - } - } catch (Exception e) { - log.debug("Could not load icon from " + path, e); - } - } - - if (icon != null) { - frame.setIconImage(icon); - setupTrayIcon(icon); - } else { - log.warn("Could not load icon from any source"); - } - } catch (Exception e) { - log.error("Error loading icon", e); - } - } - - @PreDestroy - public void cleanup() { - if (browser != null) browser.close(true); - if (client != null) client.dispose(); - if (cefApp != null) cefApp.dispose(); - if (loadingWindow != null) loadingWindow.dispose(); - } - - public static void forceInitializeUI() { - try { - if (loadingWindow != null) { - log.info("Forcing start of UI initialization sequence"); - - // Close loading window first - loadingWindow.setVisible(false); - loadingWindow.dispose(); - loadingWindow = null; - log.info("Loading window disposed"); - - // Then setup the main frame - frame.setVisible(false); - frame.dispose(); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.pack(); - frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800)); - frame.setLocationRelativeTo(null); - log.debug("Frame reconfigured"); - - // Show the main frame - frame.setVisible(true); - frame.requestFocus(); - frame.toFront(); - log.info("Main frame displayed and focused"); - - // Focus the browser component if available - if (browser != null) { - Timer focusTimer = - new Timer( - 100, - e -> { - try { - browser.getUIComponent().requestFocus(); - log.info("Browser component focused"); - } catch (Exception ex) { - log.error( - "Error focusing browser during force ui" - + " initialization.", - ex); - } - }); - focusTimer.setRepeats(false); - focusTimer.start(); - } - } - } catch (Exception e) { - log.error("Error during Forced UI initialization.", e); - // Attempt cleanup on error - if (loadingWindow != null) { - loadingWindow.dispose(); - loadingWindow = null; - } - if (frame != null) { - frame.setVisible(true); - frame.setOpacity(1.0f); - frame.setUndecorated(false); - frame.requestFocus(); - } - } - } -} diff --git a/app/core/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java b/app/core/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java deleted file mode 100644 index 5c7381fa41..0000000000 --- a/app/core/src/main/java/stirling/software/SPDF/UI/impl/LoadingWindow.java +++ /dev/null @@ -1,351 +0,0 @@ -package stirling.software.SPDF.UI.impl; - -import java.awt.*; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import javax.imageio.ImageIO; -import javax.swing.*; - -import io.github.pixee.security.BoundedLineReader; - -import lombok.extern.slf4j.Slf4j; - -import stirling.software.common.util.UIScaling; - -@Slf4j -public class LoadingWindow extends JDialog { - private final JProgressBar progressBar; - private final JLabel statusLabel; - private final JPanel mainPanel; - private final JLabel brandLabel; - private long startTime; - - private Timer stuckTimer; - private long stuckThreshold = 4000; - private long timeAt90Percent = -1; - private volatile Process explorerProcess; - private static final boolean IS_WINDOWS = - System.getProperty("os.name").toLowerCase().contains("win"); - - public LoadingWindow(Frame parent, String initialUrl) { - super(parent, "Initializing Stirling-PDF", true); - startTime = System.currentTimeMillis(); - log.info("Creating LoadingWindow - initialization started at: {}", startTime); - - // Initialize components - mainPanel = new JPanel(); - mainPanel.setBackground(Color.WHITE); - mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30)); - mainPanel.setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - - // Configure GridBagConstraints - gbc.gridwidth = GridBagConstraints.REMAINDER; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(5, 5, 5, 5); - gbc.weightx = 1.0; - gbc.weighty = 0.0; - - // Add icon - try { - try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) { - if (is != null) { - Image img = ImageIO.read(is); - if (img != null) { - Image scaledImg = UIScaling.scaleIcon(img, 48, 48); - JLabel iconLabel = new JLabel(new ImageIcon(scaledImg)); - iconLabel.setHorizontalAlignment(SwingConstants.CENTER); - gbc.gridy = 0; - mainPanel.add(iconLabel, gbc); - log.info("Icon loaded and scaled successfully"); - } - } - } - } catch (Exception e) { - log.error("Failed to load icon", e); - } - - // URL Label with explicit size - brandLabel = new JLabel(initialUrl); - brandLabel.setHorizontalAlignment(SwingConstants.CENTER); - brandLabel.setPreferredSize(new Dimension(300, 25)); - brandLabel.setText("Stirling-PDF"); - gbc.gridy = 1; - mainPanel.add(brandLabel, gbc); - - // Status label with explicit size - statusLabel = new JLabel("Initializing..."); - statusLabel.setHorizontalAlignment(SwingConstants.CENTER); - statusLabel.setPreferredSize(new Dimension(300, 25)); - gbc.gridy = 2; - mainPanel.add(statusLabel, gbc); - - // Progress bar with explicit size - progressBar = new JProgressBar(0, 100); - progressBar.setStringPainted(true); - progressBar.setPreferredSize(new Dimension(300, 25)); - gbc.gridy = 3; - mainPanel.add(progressBar, gbc); - - // Set dialog properties - setContentPane(mainPanel); - setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE); - setResizable(false); - setUndecorated(false); - - // Set size and position - setSize(UIScaling.scaleWidth(400), UIScaling.scaleHeight(200)); - - setLocationRelativeTo(parent); - setAlwaysOnTop(true); - setProgress(0); - setStatus("Starting..."); - - log.info( - "LoadingWindow initialization completed in {}ms", - System.currentTimeMillis() - startTime); - } - - private void checkAndRefreshExplorer() { - if (!IS_WINDOWS) { - return; - } - if (timeAt90Percent == -1) { - timeAt90Percent = System.currentTimeMillis(); - stuckTimer = - new Timer( - 1000, - e -> { - long currentTime = System.currentTimeMillis(); - if (currentTime - timeAt90Percent > stuckThreshold) { - try { - log.debug( - "Attempting Windows explorer refresh due to 90% stuck state"); - String currentDir = System.getProperty("user.dir"); - - // Store current explorer PIDs before we start new one - Set existingPids = new HashSet<>(); - ProcessBuilder listExplorer = - new ProcessBuilder( - "cmd", - "/c", - "wmic", - "process", - "where", - "name='explorer.exe'", - "get", - "ProcessId", - "/format:csv"); - Process process = listExplorer.start(); - BufferedReader reader = - new BufferedReader( - new InputStreamReader( - process.getInputStream())); - String line; - while ((line = - BoundedLineReader.readLine( - reader, 5_000_000)) - != null) { - if (line.matches(".*\\d+.*")) { // Contains numbers - String[] parts = line.trim().split(","); - if (parts.length >= 2) { - existingPids.add( - parts[parts.length - 1].trim()); - } - } - } - process.waitFor(2, TimeUnit.SECONDS); - - // Start new explorer - ProcessBuilder pb = - new ProcessBuilder( - "cmd", - "/c", - "start", - "/min", - "/b", - "explorer.exe", - currentDir); - pb.redirectErrorStream(true); - explorerProcess = pb.start(); - - // Schedule cleanup - Timer cleanupTimer = - new Timer( - 2000, - cleanup -> { - try { - // Find new explorer processes - ProcessBuilder findNewExplorer = - new ProcessBuilder( - "cmd", - "/c", - "wmic", - "process", - "where", - "name='explorer.exe'", - "get", - "ProcessId", - "/format:csv"); - Process newProcess = - findNewExplorer.start(); - BufferedReader newReader = - new BufferedReader( - new InputStreamReader( - newProcess - .getInputStream())); - String newLine; - while ((newLine = - BoundedLineReader - .readLine( - newReader, - 5_000_000)) - != null) { - if (newLine.matches( - ".*\\d+.*")) { - String[] parts = - newLine.trim() - .split(","); - if (parts.length >= 2) { - String pid = - parts[ - parts.length - - 1] - .trim(); - if (!existingPids - .contains( - pid)) { - log.debug( - "Found new explorer.exe with PID: " - + pid); - ProcessBuilder - killProcess = - new ProcessBuilder( - "taskkill", - "/PID", - pid, - "/F"); - killProcess - .redirectErrorStream( - true); - Process killResult = - killProcess - .start(); - killResult.waitFor( - 2, - TimeUnit - .SECONDS); - log.debug( - "Explorer process terminated: " - + pid); - } - } - } - } - newProcess.waitFor( - 2, TimeUnit.SECONDS); - } catch (Exception ex) { - log.error( - "Error cleaning up Windows explorer process", - ex); - } - }); - cleanupTimer.setRepeats(false); - cleanupTimer.start(); - stuckTimer.stop(); - } catch (Exception ex) { - log.error("Error refreshing Windows explorer", ex); - } - } - }); - stuckTimer.setRepeats(true); - stuckTimer.start(); - } - } - - public void setProgress(final int progress) { - SwingUtilities.invokeLater( - () -> { - try { - int validProgress = Math.min(Math.max(progress, 0), 100); - log.info( - "Setting progress to {}% at {}ms since start", - validProgress, System.currentTimeMillis() - startTime); - - // Log additional details when near 90% - if (validProgress >= 85 && validProgress <= 95) { - log.info( - "Near 90% progress - Current status: {}, Window visible: {}, " - + "Progress bar responding: {}, Memory usage: {}MB", - statusLabel.getText(), - isVisible(), - progressBar.isEnabled(), - Runtime.getRuntime().totalMemory() / (1024 * 1024)); - - // Add thread state logging - Thread currentThread = Thread.currentThread(); - log.info( - "Current thread state - Name: {}, State: {}, Priority: {}", - currentThread.getName(), - currentThread.getState(), - currentThread.getPriority()); - - if (validProgress >= 90 && validProgress < 95) { - checkAndRefreshExplorer(); - } else { - // Reset the timer if we move past 95% - if (validProgress >= 95) { - if (stuckTimer != null) { - stuckTimer.stop(); - } - timeAt90Percent = -1; - } - } - } - - progressBar.setValue(validProgress); - progressBar.setString(validProgress + "%"); - mainPanel.revalidate(); - mainPanel.repaint(); - } catch (Exception e) { - log.error("Error updating progress to " + progress, e); - } - }); - } - - public void setStatus(final String status) { - log.info( - "Status update at {}ms - Setting status to: {}", - System.currentTimeMillis() - startTime, - status); - - SwingUtilities.invokeLater( - () -> { - try { - String validStatus = status != null ? status : ""; - statusLabel.setText(validStatus); - - // Log UI state when status changes - log.info( - "UI State - Window visible: {}, Progress: {}%, Status: {}", - isVisible(), progressBar.getValue(), validStatus); - - mainPanel.revalidate(); - mainPanel.repaint(); - } catch (Exception e) { - log.error("Error updating status to: " + status, e); - } - }); - } - - @Override - public void dispose() { - log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime); - super.dispose(); - } -} diff --git a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index c33b869487..bdcd183a2f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -45,7 +45,13 @@ public class CleanUrlInterceptor implements HandlerInterceptor { String queryString = request.getQueryString(); if (queryString != null && !queryString.isEmpty()) { - + String requestURI = request.getRequestURI(); + + if (requestURI.contains("/api/")) { + return true; + } + + Map allowedParameters = new HashMap<>(); // Keep only the allowed parameters diff --git a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 1a5635baa1..c0a0b6b55c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -53,14 +53,14 @@ public class OpenApiConfig { .description(DEFAULT_DESCRIPTION); OpenAPI openAPI = new OpenAPI().info(info); - + // Add server configuration from environment variable String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL"); if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) { Server server = new Server().url(swaggerServerUrl).description("API Server"); openAPI.addServersItem(server); } - + if (!applicationProperties.getSecurity().getEnableLogin()) { return openAPI.components(new Components()); } else { @@ -69,8 +69,7 @@ public class OpenApiConfig { .type(SecurityScheme.Type.APIKEY) .in(SecurityScheme.In.HEADER) .name("X-API-KEY"); - return openAPI - .components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) + return openAPI.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) .addSecurityItem(new SecurityRequirement().addList("apiKey")); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index c3e204b3cb..dab11c697b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -2,13 +2,10 @@ package stirling.software.SPDF.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import lombok.RequiredArgsConstructor; -import stirling.software.common.configuration.InstallationPathConfig; - @Configuration @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { @@ -20,12 +17,12 @@ public class WebMvcConfig implements WebMvcConfigurer { registry.addInterceptor(endpointInterceptor); } - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - // Handler for external static resources - registry.addResourceHandler("/**") - .addResourceLocations( - "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/"); - // .setCachePeriod(0); // Optional: disable caching - } + // @Override + // public void addResourceHandlers(ResourceHandlerRegistry registry) { + // // Handler for external static resources - DISABLED in backend-only mode + // registry.addResourceHandler("/**") + // .addResourceLocations( + // "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/"); + // // .setCachePeriod(0); // Optional: disable caching + // } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java index 27fff75d2f..fe1440d4ae 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/AnalysisController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.*; @@ -20,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java index f47bd0d0c5..9a7f09c5e2 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -14,7 +12,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.CropPdfForm; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java index 2f823695ec..bce16e8673 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/EditTableOfContentsController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.HashMap; @@ -16,7 +14,6 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -33,6 +30,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.EditTableOfContentsRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 538b82a901..23a03e59a3 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -24,7 +22,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -36,6 +33,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.general.MergePdfsRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index d07b5314ef..96b23f3325 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -15,7 +13,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -27,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java index 5c262ecc60..c4dba6eb39 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java @@ -1,14 +1,11 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,6 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.service.PdfImageRemovalService; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 4f90ddac00..12c2674952 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -17,7 +15,6 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -29,6 +26,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index 6254183b09..31274ac23b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -11,7 +9,6 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -26,6 +23,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.general.RearrangePagesRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index e5a8ae90c7..bc4062a34f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -9,7 +7,6 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageTree; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -21,6 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.RotatePDFRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index abc4c4e462..1638e1b246 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; @@ -16,7 +14,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -28,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.general.ScalePagesRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index efa77b54ae..a1cedd5402 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.Map; @@ -9,7 +7,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,6 +16,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.InstallationPathConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 32691b2d0e..e3a9e15c6c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -17,7 +15,6 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -30,6 +27,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.PDFWithPageNums; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index a30b208c9e..e519344da9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.nio.file.Files; import java.nio.file.Path; @@ -17,7 +15,6 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -34,6 +31,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.PdfMetadata; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.PdfMetadataService; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index a27d7f1b9d..6d3ae9efe8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -22,7 +20,6 @@ import org.apache.pdfbox.util.Matrix; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -34,6 +31,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index adfe42b464..5ff32f286b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -14,7 +12,6 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -27,6 +24,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index e52f1bfefa..1b5f1a7501 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.geom.AffineTransform; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -14,7 +12,6 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -23,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java index 3161560aeb..0902cb1f47 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/UIDataController.java @@ -25,7 +25,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Data; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.Dependency; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java index 7633dd1100..f1aa76f37d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEmlToPDF.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -10,7 +8,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index 15abcf50f1..32d1347d6d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -1,10 +1,7 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -15,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.converters.HTMLToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index 39af4a002f..ba58d951fa 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -21,7 +19,6 @@ import org.apache.pdfbox.rendering.ImageType; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -35,6 +32,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index 0bd4cf6dda..2e0f53a0ff 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.util.List; import java.util.Map; @@ -14,7 +12,6 @@ import org.commonmark.renderer.html.AttributeProvider; import org.commonmark.renderer.html.HtmlRenderer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -25,6 +22,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index eb1eecc59b..339f184b36 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -15,7 +13,6 @@ import org.apache.commons.io.FilenameUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -26,6 +23,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.api.GeneralFile; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java index 6b47a498b3..63c96d762d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToHtml.java @@ -1,10 +1,7 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -12,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.util.PDFToFile; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 3d133f9439..ee1b6eaab9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -9,7 +7,6 @@ import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -23,6 +20,7 @@ import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; import stirling.software.SPDF.model.api.converters.PdfToWordRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PDFToFile; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index 1e77e2b449..0e5aeb1f6a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.File; @@ -57,7 +55,6 @@ import org.apache.xmpbox.xml.XmpSerializer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -69,6 +66,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.converters.PdfToPdfARequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ProcessExecutor; import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index 1b5467587e..182dc8a3e5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -11,7 +9,6 @@ import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -22,6 +19,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java index 2bec58d386..1ce2e6fc3a 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractCSVController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringWriter; @@ -20,7 +18,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -32,6 +29,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.pdf.FlexibleCSVWriter; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import technology.tabula.ObjectExtractor; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index b4e9dc2857..262c931dfb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.filters; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -9,7 +7,6 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -26,6 +23,7 @@ import stirling.software.SPDF.model.api.filter.ContainsTextRequest; import stirling.software.SPDF.model.api.filter.FileSizeRequest; import stirling.software.SPDF.model.api.filter.PageRotationRequest; import stirling.software.SPDF.model.api.filter.PageSizeRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java index 3729af9d87..c6a274f5c5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AttachmentController.java @@ -1,14 +1,11 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.AddAttachmentRequest; import stirling.software.SPDF.service.AttachmentServiceInterface; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index 628e0d028a..214d0870aa 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; @@ -12,7 +10,6 @@ import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -25,6 +22,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index 0650481bf8..78518617a5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; @@ -21,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -37,6 +34,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index 010d6d0bba..f6f9f94ad0 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -19,7 +17,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -32,6 +29,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 9b0b43dc11..20a869c2cb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; @@ -36,7 +34,6 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -54,6 +51,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java index fb1b7b2ca7..bc91744763 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ConfigController.java @@ -110,7 +110,8 @@ public class ConfigController { } @GetMapping("/endpoint-enabled") - public ResponseEntity isEndpointEnabled(@RequestParam(name = "endpoint") String endpoint) { + public ResponseEntity isEndpointEnabled( + @RequestParam(name = "endpoint") String endpoint) { boolean enabled = endpointConfiguration.isEndpointEnabled(endpoint); return ResponseEntity.ok(enabled); } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java index b5fe504c4b..93dc8ee564 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/DecompressPdfController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -15,7 +13,6 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -26,6 +23,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index aa3c405197..d40a6a2924 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.io.IOException; @@ -21,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -33,6 +30,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 2e2968c9ca..026dd1b701 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; @@ -29,7 +27,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +39,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.PDFExtractImagesRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ImageProcessingUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java index ecd263c1ce..984871185f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.image.BufferedImage; import java.io.IOException; @@ -15,7 +13,6 @@ import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -28,6 +25,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.FlattenRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index 37b1c209ef..0253ccdf4d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -25,6 +23,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.MetadataRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 5cd80384cc..a1b2fa2770 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.image.BufferedImage; import java.io.*; import java.nio.file.Files; @@ -20,7 +18,6 @@ import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -34,6 +31,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index 5b61f66eac..8c9a994367 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -1,13 +1,10 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -20,6 +17,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.OverlayImageRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index d91c30bae7..8647358806 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; @@ -15,7 +13,6 @@ import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -27,6 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java index 34ed585405..5fc54a7992 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index e1084a457d..a5c1d79cc8 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -1,14 +1,11 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -21,6 +18,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.EndpointConfiguration; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ProcessExecutor; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java index b935d59da1..e8c864d5c4 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ReplaceAndInvertColorController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.springframework.core.io.InputStreamResource; @@ -9,7 +7,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,6 +17,7 @@ import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest; import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService; +import stirling.software.common.annotations.AutoJobPostMapping; @RestController @RequestMapping("/api/v1/misc") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java index a140e90295..f7fe0e837e 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; @@ -21,7 +19,6 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -36,6 +33,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ScannerEffectRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 709d8bd09f..56f43ca416 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.nio.charset.StandardCharsets; import java.util.Map; @@ -11,7 +9,6 @@ import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 8193c7a811..590ace5d4f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; @@ -28,7 +26,6 @@ import org.apache.pdfbox.util.Matrix; import org.springframework.core.io.ClassPathResource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -40,6 +37,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.misc.AddStampRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java index d80c35022f..d1394d3663 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/UnlockPDFFormsController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.misc; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; @@ -14,7 +12,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -24,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index 166668db9f..ac034e287d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.pipeline; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.HashMap; @@ -14,7 +12,6 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -32,6 +29,7 @@ import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.model.api.HandleDataRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.PostHogService; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index b43d918e8b..316311d874 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.beans.PropertyEditorSupport; import java.io.*; @@ -61,7 +59,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -75,6 +72,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 436b2ba302..8d5f6fb373 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -48,7 +46,6 @@ import org.apache.xmpbox.xml.XmpSerializer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -63,6 +60,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index d3e78ef6e9..566a1224ae 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; @@ -9,7 +7,6 @@ import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.StandardProtectionPolicy; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +19,7 @@ import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 23d7e20ad9..d73381f198 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -20,7 +18,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -36,6 +33,7 @@ import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.pdf.TextFinder; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.security.RedactionArea; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.GeneralUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java index 8ecfe7cb78..c07656d688 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -11,7 +9,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +19,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index 5935a5152a..af6b30a473 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.io.IOException; import org.apache.pdfbox.cos.COSDictionary; @@ -25,7 +23,6 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -37,6 +34,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.security.SanitizePdfRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java index 0ed9e98f93..7ce340fbcb 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.beans.PropertyEditorSupport; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -29,7 +27,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +39,7 @@ import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.security.SignatureValidationRequest; import stirling.software.SPDF.model.api.security.SignatureValidationResult; import stirling.software.SPDF.service.CertificateValidationService; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.ExceptionUtils; @@ -71,7 +69,9 @@ public class ValidateSignatureController { description = "Validates the digital signatures in a PDF file against default or custom" + " certificates. Input:PDF Output:JSON Type:SISO") - @AutoJobPostMapping(value = "/validate-signature", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @AutoJobPostMapping( + value = "/validate-signature", + consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> validateSignature( @ModelAttribute SignatureValidationRequest request) throws IOException { List results = new ArrayList<>(); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index b3abf2df7e..e94559658c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.controller.api.security; -import stirling.software.common.annotations.AutoJobPostMapping; - import java.awt.*; import java.awt.image.BufferedImage; import java.beans.PropertyEditorSupport; @@ -30,7 +28,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -42,6 +39,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import stirling.software.SPDF.model.api.security.AddWatermarkRequest; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 1c05aaabdd..bec23184c5 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -1,8 +1,6 @@ package stirling.software.SPDF.controller.web; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import io.swagger.v3.oas.annotations.Hidden; @@ -12,46 +10,52 @@ import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Tag(name = "Convert", description = "Convert APIs") public class ConverterWebController { - @GetMapping("/img-to-pdf") + @Deprecated + // @GetMapping("/img-to-pdf") @Hidden public String convertImgToPdfForm(Model model) { model.addAttribute("currentPage", "img-to-pdf"); return "convert/img-to-pdf"; } - @GetMapping("/html-to-pdf") + @Deprecated + // @GetMapping("/html-to-pdf") @Hidden public String convertHTMLToPdfForm(Model model) { model.addAttribute("currentPage", "html-to-pdf"); return "convert/html-to-pdf"; } - @GetMapping("/markdown-to-pdf") + @Deprecated + // @GetMapping("/markdown-to-pdf") @Hidden public String convertMarkdownToPdfForm(Model model) { model.addAttribute("currentPage", "markdown-to-pdf"); return "convert/markdown-to-pdf"; } - @GetMapping("/pdf-to-markdown") + @Deprecated + // @GetMapping("/pdf-to-markdown") @Hidden public String convertPdfToMarkdownForm(Model model) { model.addAttribute("currentPage", "pdf-to-markdown"); return "convert/pdf-to-markdown"; } - @GetMapping("/url-to-pdf") + @Deprecated + // @GetMapping("/url-to-pdf") @Hidden public String convertURLToPdfForm(Model model) { model.addAttribute("currentPage", "url-to-pdf"); return "convert/url-to-pdf"; } - @GetMapping("/file-to-pdf") + @Deprecated + // @GetMapping("/file-to-pdf") @Hidden public String convertToPdfForm(Model model) { model.addAttribute("currentPage", "file-to-pdf"); @@ -60,7 +64,8 @@ public class ConverterWebController { // PDF TO...... - @GetMapping("/pdf-to-img") + @Deprecated + // @GetMapping("/pdf-to-img") @Hidden public String pdfToimgForm(Model model) { boolean isPython = CheckProgramInstall.isPythonAvailable(); @@ -76,7 +81,8 @@ public class ConverterWebController { return "convert/pdf-to-img"; } - @GetMapping("/pdf-to-html") + @Deprecated + // @GetMapping("/pdf-to-html") @Hidden public ModelAndView pdfToHTML() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html"); @@ -84,7 +90,8 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-presentation") + @Deprecated + // @GetMapping("/pdf-to-presentation") @Hidden public ModelAndView pdfToPresentation() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation"); @@ -92,7 +99,8 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-text") + @Deprecated + // @GetMapping("/pdf-to-text") @Hidden public ModelAndView pdfToText() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text"); @@ -100,7 +108,8 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-word") + @Deprecated + // @GetMapping("/pdf-to-word") @Hidden public ModelAndView pdfToWord() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word"); @@ -108,7 +117,8 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-xml") + @Deprecated + // @GetMapping("/pdf-to-xml") @Hidden public ModelAndView pdfToXML() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml"); @@ -116,7 +126,8 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-csv") + @Deprecated + // @GetMapping("/pdf-to-csv") @Hidden public ModelAndView pdfToCSV() { ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv"); @@ -124,14 +135,16 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-pdfa") + @Deprecated + // @GetMapping("/pdf-to-pdfa") @Hidden public String pdfToPdfAForm(Model model) { model.addAttribute("currentPage", "pdf-to-pdfa"); return "convert/pdf-to-pdfa"; } - @GetMapping("/eml-to-pdf") + @Deprecated + // @GetMapping("/eml-to-pdf") @Hidden public String convertEmlToPdfForm(Model model) { model.addAttribute("currentPage", "eml-to-pdf"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 1084e2fe07..b8e6036f93 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -12,9 +12,7 @@ import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -32,7 +30,7 @@ import stirling.software.common.service.UserServiceInterface; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Tag(name = "General", description = "General APIs") @Slf4j public class GeneralWebController { @@ -53,7 +51,8 @@ public class GeneralWebController { this.runtimePathConfig = runtimePathConfig; } - @GetMapping("/pipeline") + @Deprecated + // @GetMapping("/pipeline") @Hidden public String pipelineForm(Model model) { model.addAttribute("currentPage", "pipeline"); @@ -103,91 +102,104 @@ public class GeneralWebController { return "pipeline"; } - @GetMapping("/merge-pdfs") + @Deprecated + // @GetMapping("/merge-pdfs") @Hidden public String mergePdfForm(Model model) { model.addAttribute("currentPage", "merge-pdfs"); return "merge-pdfs"; } - @GetMapping("/split-pdf-by-sections") + @Deprecated + // @GetMapping("/split-pdf-by-sections") @Hidden public String splitPdfBySections(Model model) { model.addAttribute("currentPage", "split-pdf-by-sections"); return "split-pdf-by-sections"; } - @GetMapping("/split-pdf-by-chapters") + @Deprecated + // @GetMapping("/split-pdf-by-chapters") @Hidden public String splitPdfByChapters(Model model) { model.addAttribute("currentPage", "split-pdf-by-chapters"); return "split-pdf-by-chapters"; } - @GetMapping("/view-pdf") + @Deprecated + // @GetMapping("/view-pdf") @Hidden public String ViewPdfForm2(Model model) { model.addAttribute("currentPage", "view-pdf"); return "view-pdf"; } - @GetMapping("/edit-table-of-contents") + @Deprecated + // @GetMapping("/edit-table-of-contents") @Hidden public String editTableOfContents(Model model) { model.addAttribute("currentPage", "edit-table-of-contents"); return "edit-table-of-contents"; } - @GetMapping("/multi-tool") + @Deprecated + // @GetMapping("/multi-tool") @Hidden public String multiToolForm(Model model) { model.addAttribute("currentPage", "multi-tool"); return "multi-tool"; } - @GetMapping("/remove-pages") + @Deprecated + // @GetMapping("/remove-pages") @Hidden public String pageDeleter(Model model) { model.addAttribute("currentPage", "remove-pages"); return "remove-pages"; } - @GetMapping("/pdf-organizer") + @Deprecated + // @GetMapping("/pdf-organizer") @Hidden public String pageOrganizer(Model model) { model.addAttribute("currentPage", "pdf-organizer"); return "pdf-organizer"; } - @GetMapping("/extract-page") + @Deprecated + // @GetMapping("/extract-page") @Hidden public String extractPages(Model model) { model.addAttribute("currentPage", "extract-page"); return "extract-page"; } - @GetMapping("/pdf-to-single-page") + @Deprecated + // @GetMapping("/pdf-to-single-page") @Hidden public String pdfToSinglePage(Model model) { model.addAttribute("currentPage", "pdf-to-single-page"); return "pdf-to-single-page"; } - @GetMapping("/rotate-pdf") + @Deprecated + // @GetMapping("/rotate-pdf") @Hidden public String rotatePdfForm(Model model) { model.addAttribute("currentPage", "rotate-pdf"); return "rotate-pdf"; } - @GetMapping("/split-pdfs") + @Deprecated + // @GetMapping("/split-pdfs") @Hidden public String splitPdfForm(Model model) { model.addAttribute("currentPage", "split-pdfs"); return "split-pdfs"; } - @GetMapping("/sign") + @Deprecated + // @GetMapping("/sign") @Hidden public String signForm(Model model) { String username = ""; @@ -202,28 +214,32 @@ public class GeneralWebController { return "sign"; } - @GetMapping("/multi-page-layout") + @Deprecated + // @GetMapping("/multi-page-layout") @Hidden public String multiPageLayoutForm(Model model) { model.addAttribute("currentPage", "multi-page-layout"); return "multi-page-layout"; } - @GetMapping("/scale-pages") + @Deprecated + // @GetMapping("/scale-pages") @Hidden public String scalePagesFrom(Model model) { model.addAttribute("currentPage", "scale-pages"); return "scale-pages"; } - @GetMapping("/split-by-size-or-count") + @Deprecated + // @GetMapping("/split-by-size-or-count") @Hidden public String splitBySizeOrCount(Model model) { model.addAttribute("currentPage", "split-by-size-or-count"); return "split-by-size-or-count"; } - @GetMapping("/overlay-pdf") + @Deprecated + // @GetMapping("/overlay-pdf") @Hidden public String overlayPdf(Model model) { model.addAttribute("currentPage", "overlay-pdf"); @@ -296,21 +312,24 @@ public class GeneralWebController { } } - @GetMapping("/crop") + @Deprecated + // @GetMapping("/crop") @Hidden public String cropForm(Model model) { model.addAttribute("currentPage", "crop"); return "crop"; } - @GetMapping("/auto-split-pdf") + @Deprecated + // @GetMapping("/auto-split-pdf") @Hidden public String autoSPlitPDFForm(Model model) { model.addAttribute("currentPage", "auto-split-pdf"); return "auto-split-pdf"; } - @GetMapping("/remove-image-pdf") + @Deprecated + // @GetMapping("/remove-image-pdf") @Hidden public String removeImagePdfForm(Model model) { model.addAttribute("currentPage", "remove-image-pdf"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java index 2b36f95af7..e9b085dd91 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -9,7 +9,6 @@ import java.util.Map; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; @@ -26,20 +25,22 @@ import stirling.software.SPDF.model.Dependency; import stirling.software.common.model.ApplicationProperties; @Slf4j -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @RequiredArgsConstructor public class HomeWebController { private final ApplicationProperties applicationProperties; - @GetMapping("/about") + @Deprecated + // @GetMapping("/about") @Hidden public String gameForm(Model model) { model.addAttribute("currentPage", "about"); return "about"; } - @GetMapping("/licenses") + @Deprecated + // @GetMapping("/licenses") @Hidden public String licensesForm(Model model) { model.addAttribute("currentPage", "licenses"); @@ -56,12 +57,14 @@ public class HomeWebController { return "licenses"; } - @GetMapping("/releases") + @Deprecated + // @GetMapping("/releases") public String getReleaseNotes(Model model) { return "releases"; } - @GetMapping("/") + @Deprecated + // @GetMapping("/") public String home(Model model) { model.addAttribute("currentPage", "home"); String showSurvey = System.getenv("SHOW_SURVEY"); @@ -70,12 +73,14 @@ public class HomeWebController { return "home"; } - @GetMapping("/home") + @Deprecated + // @GetMapping("/home") public String root(Model model) { return "redirect:/"; } - @GetMapping("/home-legacy") + @Deprecated + // @GetMapping("/home-legacy") public String redirectHomeLegacy() { return "redirect:/"; } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index bc63a4b84a..6ef6fc7732 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -5,9 +5,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.servlet.ModelAndView; import io.swagger.v3.oas.annotations.Hidden; @@ -18,28 +16,31 @@ import lombok.RequiredArgsConstructor; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.CheckProgramInstall; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Tag(name = "Misc", description = "Miscellaneous APIs") @RequiredArgsConstructor public class OtherWebController { private final ApplicationProperties applicationProperties; - @GetMapping("/compress-pdf") + @Deprecated + // @GetMapping("/compress-pdf") @Hidden public String compressPdfForm(Model model) { model.addAttribute("currentPage", "compress-pdf"); return "misc/compress-pdf"; } - @GetMapping("/replace-and-invert-color-pdf") + @Deprecated + // @GetMapping("/replace-and-invert-color-pdf") @Hidden public String replaceAndInvertColorPdfForm(Model model) { model.addAttribute("currentPage", "replace-invert-color-pdf"); return "misc/replace-color"; } - @GetMapping("/extract-image-scans") + @Deprecated + // @GetMapping("/extract-image-scans") @Hidden public ModelAndView extractImageScansForm() { ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans"); @@ -49,70 +50,80 @@ public class OtherWebController { return modelAndView; } - @GetMapping("/show-javascript") + @Deprecated + // @GetMapping("/show-javascript") @Hidden public String extractJavascriptForm(Model model) { model.addAttribute("currentPage", "show-javascript"); return "misc/show-javascript"; } - @GetMapping("/stamp") + @Deprecated + // @GetMapping("/stamp") @Hidden public String stampForm(Model model) { model.addAttribute("currentPage", "stamp"); return "misc/stamp"; } - @GetMapping("/add-page-numbers") + @Deprecated + // @GetMapping("/add-page-numbers") @Hidden public String addPageNumbersForm(Model model) { model.addAttribute("currentPage", "add-page-numbers"); return "misc/add-page-numbers"; } - @GetMapping("/scanner-effect") + @Deprecated + // @GetMapping("/scanner-effect") @Hidden public String scannerEffectForm(Model model) { model.addAttribute("currentPage", "scanner-effect"); return "misc/scanner-effect"; } - @GetMapping("/extract-images") + @Deprecated + // @GetMapping("/extract-images") @Hidden public String extractImagesForm(Model model) { model.addAttribute("currentPage", "extract-images"); return "misc/extract-images"; } - @GetMapping("/flatten") + @Deprecated + // @GetMapping("/flatten") @Hidden public String flattenForm(Model model) { model.addAttribute("currentPage", "flatten"); return "misc/flatten"; } - @GetMapping("/change-metadata") + @Deprecated + // @GetMapping("/change-metadata") @Hidden public String addWatermarkForm(Model model) { model.addAttribute("currentPage", "change-metadata"); return "misc/change-metadata"; } - @GetMapping("/unlock-pdf-forms") + @Deprecated + // @GetMapping("/unlock-pdf-forms") @Hidden public String unlockPDFForms(Model model) { model.addAttribute("currentPage", "unlock-pdf-forms"); return "misc/unlock-pdf-forms"; } - @GetMapping("/compare") + @Deprecated + // @GetMapping("/compare") @Hidden public String compareForm(Model model) { model.addAttribute("currentPage", "compare"); return "misc/compare"; } - @GetMapping("/print-file") + @Deprecated + // @GetMapping("/print-file") @Hidden public String printFileForm(Model model) { model.addAttribute("currentPage", "print-file"); @@ -133,7 +144,8 @@ public class OtherWebController { .toList(); } - @GetMapping("/ocr-pdf") + @Deprecated + // @GetMapping("/ocr-pdf") @Hidden public ModelAndView ocrPdfPage() { ModelAndView modelAndView = new ModelAndView("misc/ocr-pdf"); @@ -143,56 +155,64 @@ public class OtherWebController { return modelAndView; } - @GetMapping("/add-image") + @Deprecated + // @GetMapping("/add-image") @Hidden public String overlayImage(Model model) { model.addAttribute("currentPage", "add-image"); return "misc/add-image"; } - @GetMapping("/adjust-contrast") + @Deprecated + // @GetMapping("/adjust-contrast") @Hidden public String contrast(Model model) { model.addAttribute("currentPage", "adjust-contrast"); return "misc/adjust-contrast"; } - @GetMapping("/repair") + @Deprecated + // @GetMapping("/repair") @Hidden public String repairForm(Model model) { model.addAttribute("currentPage", "repair"); return "misc/repair"; } - @GetMapping("/remove-blanks") + @Deprecated + // @GetMapping("/remove-blanks") @Hidden public String removeBlanksForm(Model model) { model.addAttribute("currentPage", "remove-blanks"); return "misc/remove-blanks"; } - @GetMapping("/remove-annotations") + @Deprecated + // @GetMapping("/remove-annotations") @Hidden public String removeAnnotationsForm(Model model) { model.addAttribute("currentPage", "remove-annotations"); return "misc/remove-annotations"; } - @GetMapping("/auto-crop") + @Deprecated + // @GetMapping("/auto-crop") @Hidden public String autoCropForm(Model model) { model.addAttribute("currentPage", "auto-crop"); return "misc/auto-crop"; } - @GetMapping("/auto-rename") + @Deprecated + // @GetMapping("/auto-rename") @Hidden public String autoRenameForm(Model model) { model.addAttribute("currentPage", "auto-rename"); return "misc/auto-rename"; } - @GetMapping("/add-attachments") + @Deprecated + // @GetMapping("/add-attachments") @Hidden public String attachmentsForm(Model model) { model.addAttribute("currentPage", "add-attachments"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java index 2d4ad3c977..09e40b133f 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ReactRoutingController.java @@ -1,9 +1,8 @@ package stirling.software.SPDF.controller.web; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI public class ReactRoutingController { @GetMapping("/{path:^(?!api|static|robots\\.txt|favicon\\.ico)[^\\.]*$}") diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index eb7245e5ad..e2a34e140c 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -1,86 +1,95 @@ package stirling.software.SPDF.controller.web; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Tag(name = "Security", description = "Security APIs") public class SecurityWebController { - @GetMapping("/auto-redact") + @Deprecated + // @GetMapping("/auto-redact") @Hidden public String autoRedactForm(Model model) { model.addAttribute("currentPage", "auto-redact"); return "security/auto-redact"; } - @GetMapping("/redact") + @Deprecated + // @GetMapping("/redact") public String redactForm(Model model) { model.addAttribute("currentPage", "redact"); return "security/redact"; } - @GetMapping("/add-password") + @Deprecated + // @GetMapping("/add-password") @Hidden public String addPasswordForm(Model model) { model.addAttribute("currentPage", "add-password"); return "security/add-password"; } - @GetMapping("/change-permissions") + @Deprecated + // @GetMapping("/change-permissions") @Hidden public String permissionsForm(Model model) { model.addAttribute("currentPage", "change-permissions"); return "security/change-permissions"; } - @GetMapping("/remove-password") + @Deprecated + // @GetMapping("/remove-password") @Hidden public String removePasswordForm(Model model) { model.addAttribute("currentPage", "remove-password"); return "security/remove-password"; } - @GetMapping("/add-watermark") + @Deprecated + // @GetMapping("/add-watermark") @Hidden public String addWatermarkForm(Model model) { model.addAttribute("currentPage", "add-watermark"); return "security/add-watermark"; } - @GetMapping("/cert-sign") + @Deprecated + // @GetMapping("/cert-sign") @Hidden public String certSignForm(Model model) { model.addAttribute("currentPage", "cert-sign"); return "security/cert-sign"; } - @GetMapping("/validate-signature") + @Deprecated + // @GetMapping("/validate-signature") @Hidden public String certSignVerifyForm(Model model) { model.addAttribute("currentPage", "validate-signature"); return "security/validate-signature"; } - @GetMapping("/remove-cert-sign") + @Deprecated + // @GetMapping("/remove-cert-sign") @Hidden public String certUnSignForm(Model model) { model.addAttribute("currentPage", "remove-cert-sign"); return "security/remove-cert-sign"; } - @GetMapping("/sanitize-pdf") + @Deprecated + // @GetMapping("/sanitize-pdf") @Hidden public String sanitizeForm(Model model) { model.addAttribute("currentPage", "sanitize-pdf"); return "security/sanitize-pdf"; } - @GetMapping("/get-info-on-pdf") + @Deprecated + // @GetMapping("/get-info-on-pdf") @Hidden public String getInfo(Model model) { model.addAttribute("currentPage", "get-info-on-pdf"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java index de03680e1b..ad046f3262 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java @@ -6,7 +6,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -14,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import stirling.software.SPDF.service.SignatureService; import stirling.software.common.service.UserServiceInterface; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @RequestMapping("/api/v1/general") public class SignatureController { diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java index a3ebcfd3d1..e9b326a2a9 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/ConvertPDFToMarkdown.java @@ -1,10 +1,7 @@ package stirling.software.SPDF.model.api.converters; -import stirling.software.common.annotations.AutoJobPostMapping; - import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -12,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.common.model.api.PDFFile; import stirling.software.common.util.PDFToFile; diff --git a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java index 1d25f409fb..16385392e4 100644 --- a/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java +++ b/app/core/src/main/java/stirling/software/SPDF/service/SignatureService.java @@ -9,7 +9,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; -import org.thymeleaf.util.StringUtils; +import org.springframework.util.StringUtils; import lombok.extern.slf4j.Slf4j; @@ -40,7 +40,7 @@ public class SignatureService { List signatures = new ArrayList<>(); // Get signatures from user's personal folder - if (!StringUtils.isEmptyOrWhitespace(username)) { + if (StringUtils.hasText(username)) { Path userFolder = Paths.get(SIGNATURE_BASE_PATH, username); if (Files.exists(userFolder)) { try { diff --git a/app/core/src/main/resources/application.properties b/app/core/src/main/resources/application.properties index ea30bf78ee..da1999dd01 100644 --- a/app/core/src/main/resources/application.properties +++ b/app/core/src/main/resources/application.properties @@ -25,7 +25,7 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/} spring.devtools.restart.enabled=true spring.devtools.livereload.enabled=true spring.devtools.restart.exclude=stirling.software.proprietary.security/** -spring.thymeleaf.encoding=UTF-8 +# spring.thymeleaf.encoding=UTF-8 # Disabled - React frontend replaces Thymeleaf spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} @@ -35,6 +35,14 @@ spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=false spring.jpa.hibernate.ddl-auto=update +# Defer datasource initialization to ensure that the database is fully set up +# before Hibernate attempts to access it. This is particularly useful when +# using database initialization scripts or tools. +spring.jpa.defer-datasource-initialization=true + +# Disable SQL logging to avoid cluttering the logs in production. Enable this +# property during development if you need to debug SQL queries. +spring.jpa.show-sql=false server.servlet.session.timeout:30m # Change the default URL path for OpenAPI JSON springdoc.api-docs.path=/v1/api-docs diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 719f741271..4a855ec6b0 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -35,6 +35,8 @@ dependencies { api 'org.springframework:spring-webmvc' api 'org.springframework.session:spring-session-core' api "org.springframework.security:spring-security-core:$springSecuritySamlVersion" + api "org.springframework.security:spring-security-web:$springSecuritySamlVersion" + api "org.springframework.security:spring-security-config:$springSecuritySamlVersion" api "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion" api 'org.springframework.boot:spring-boot-starter-jetty' api 'org.springframework.boot:spring-boot-starter-security' @@ -47,7 +49,7 @@ dependencies { // https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17 implementation 'org.bouncycastle:bcprov-jdk18on:1.81' - implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' + // implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE' // Removed - UI moved to React frontend api 'io.micrometer:micrometer-registry-prometheus' implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.0.0' runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java b/app/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java index 67b71ccd80..6d2f853038 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/controller/AuditDashboardController.java @@ -22,7 +22,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -46,7 +45,7 @@ import stirling.software.proprietary.security.config.EnterpriseEndpoint; /** Controller for the audit dashboard. Admin-only access. */ @Slf4j -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @RequestMapping("/audit") @PreAuthorize("hasRole('ADMIN')") @RequiredArgsConstructor diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java index 0d846fc3d2..1009a6abad 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java @@ -12,13 +12,10 @@ import java.util.Map; import java.util.Optional; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -48,7 +45,7 @@ import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrin import stirling.software.proprietary.security.service.TeamService; import stirling.software.proprietary.security.session.SessionPersistentRegistry; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Slf4j @Tag(name = "Account Security", description = "Account Security APIs") public class AccountWebController { @@ -75,7 +72,7 @@ public class AccountWebController { this.teamRepository = teamRepository; } - @GetMapping("/login") + // @GetMapping("/login") public String login(HttpServletRequest request, Model model, Authentication authentication) { // If the user is already authenticated, redirect them to the home page. if (authentication != null && authentication.isAuthenticated()) { @@ -203,8 +200,8 @@ public class AccountWebController { return "login"; } - @PreAuthorize("hasRole('ROLE_ADMIN')") - @GetMapping("/usage") + // @PreAuthorize("hasRole('ROLE_ADMIN')") + // @GetMapping("/usage") public String showUsage() { if (!runningEE) { return "error"; @@ -212,8 +209,8 @@ public class AccountWebController { return "usage"; } - @PreAuthorize("hasRole('ROLE_ADMIN')") - @GetMapping("/adminSettings") + // @PreAuthorize("hasRole('ROLE_ADMIN')") + // @GetMapping("/adminSettings") public String showAddUserForm( HttpServletRequest request, Model model, Authentication authentication) { List allUsers = userRepository.findAllWithTeam(); @@ -371,8 +368,8 @@ public class AccountWebController { return "adminSettings"; } - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") - @GetMapping("/account") + // @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + // @GetMapping("/account") public String account(HttpServletRequest request, Model model, Authentication authentication) { if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/"; @@ -436,8 +433,8 @@ public class AccountWebController { return "account"; } - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") - @GetMapping("/change-creds") + // @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + // @GetMapping("/change-creds") public String changeCreds( HttpServletRequest request, Model model, Authentication authentication) { if (authentication == null || !authentication.isAuthenticated()) { diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java index 839c070833..e2a55e00ef 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/EmailController.java @@ -1,13 +1,10 @@ package stirling.software.proprietary.security.controller.api; -import stirling.software.common.annotations.AutoJobPostMapping; - import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.mail.MailSendException; import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -20,6 +17,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.annotations.AutoJobPostMapping; import stirling.software.proprietary.security.model.api.Email; import stirling.software.proprietary.security.service.EmailService; diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/DatabaseWebController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/DatabaseWebController.java index 940c0c13f4..ee33be0b97 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/DatabaseWebController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/DatabaseWebController.java @@ -4,9 +4,7 @@ import java.util.List; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import io.swagger.v3.oas.annotations.tags.Tag; @@ -17,15 +15,16 @@ import lombok.RequiredArgsConstructor; import stirling.software.common.model.FileInfo; import stirling.software.proprietary.security.service.DatabaseService; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @Tag(name = "Database Management", description = "Database management and security APIs") @RequiredArgsConstructor public class DatabaseWebController { private final DatabaseService databaseService; + @Deprecated @PreAuthorize("hasRole('ROLE_ADMIN')") - @GetMapping("/database") + // @GetMapping("/database") public String database(HttpServletRequest request, Model model, Authentication authentication) { String error = request.getParameter("error"); String confirmed = request.getParameter("infoMessage"); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java index 2dd9b3478d..71ea99d2f1 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java @@ -6,9 +6,7 @@ import java.util.List; import java.util.Map; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,7 +23,7 @@ import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamRepository; import stirling.software.proprietary.security.service.TeamService; -@Controller +// @Controller // Disabled - Backend-only mode, no Thymeleaf UI @RequestMapping("/teams") @RequiredArgsConstructor @Slf4j @@ -35,7 +33,8 @@ public class TeamWebController { private final SessionRepository sessionRepository; private final UserRepository userRepository; - @GetMapping + @Deprecated + // @GetMapping @PreAuthorize("hasRole('ROLE_ADMIN')") public String listTeams(HttpServletRequest request, Model model) { // Get teams with user counts using a DTO projection @@ -86,7 +85,8 @@ public class TeamWebController { return "accounts/teams"; } - @GetMapping("/{id}") + @Deprecated + // @GetMapping("/{id}") @PreAuthorize("hasRole('ROLE_ADMIN')") public String viewTeamDetails( HttpServletRequest request, @PathVariable("id") Long id, Model model) { diff --git a/testing/compose/docker-compose-ultra-lite.yml b/testing/compose/docker-compose-ultra-lite.yml index 2ea9464a64..bddea46688 100644 --- a/testing/compose/docker-compose-ultra-lite.yml +++ b/testing/compose/docker-compose-ultra-lite.yml @@ -10,7 +10,7 @@ services: limits: memory: 2G healthcheck: - test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"] + test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"] interval: 5s timeout: 10s retries: 16 diff --git a/testing/test.sh b/testing/test.sh index 0876893dec..ec01ff638a 100644 --- a/testing/test.sh +++ b/testing/test.sh @@ -185,23 +185,18 @@ verify_app_version() { echo "Checking version for $service_name (expecting $EXPECTED_VERSION)..." - # Try to access the homepage and extract the version + # Try to access the status endpoint and extract the version local response - response=$(curl -s "$base_url") + response=$(curl -s "$base_url/api/v1/info/status") - # Extract version from pixel tracking tag + # Extract version from JSON response using grep and sed local actual_version - actual_version=$(echo "$response" | grep -o 'appVersion=[0-9.]*' | head -1 | sed 's/appVersion=//') + actual_version=$(echo "$response" | grep -o '"version":"[^"]*"' | head -1 | sed 's/"version":"//' | sed 's/"//') - # If we couldn't find the version in the pixel tag, try other approaches + # Check if we got a valid response if [ -z "$actual_version" ]; then - # Check for "App Version:" format - if echo "$response" | grep -q "App Version:"; then - actual_version=$(echo "$response" | grep -o "App Version: [0-9.]*" | sed 's/App Version: //') - else - echo "❌ Version verification failed: Could not find version information" - return 1 - fi + echo "❌ Version verification failed: Could not find version information in status endpoint" + return 1 fi # Check if the extracted version matches expected version @@ -278,15 +273,6 @@ main() { # Test each configuration run_tests "Stirling-PDF-Ultra-Lite" "./testing/compose/docker-compose-ultra-lite.yml" - - echo "Testing basic frontend homepage accessibility..." - if curl -f http://localhost:3000 > /dev/null 2>&1; then - passed_tests+=("Frontend-Homepage-Accessibility-lite") - echo "Frontend homepage accessibility check passed" - else - failed_tests+=("Frontend-Homepage-Accessibility-lite") - echo "Frontend homepage accessibility check failed" - fi # echo "Testing webpage accessibility..." # cd "testing" @@ -327,15 +313,6 @@ main() { # Test each configuration with security run_tests "Stirling-PDF-Security" "./testing/compose/docker-compose-security.yml" - - echo "Testing basic frontend homepage accessibility..." - if curl -f http://localhost:3000 > /dev/null 2>&1; then - passed_tests+=("Frontend-Homepage-Accessibility-full") - echo "Frontend homepage accessibility check passed" - else - failed_tests+=("Frontend-Homepage-Accessibility-full") - echo "Frontend homepage accessibility check failed" - fi # echo "Testing webpage accessibility..." # cd "testing" From 299e0b2475875b34f910832886bac2471aba4b08 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 12 Aug 2025 09:51:13 +0100 Subject: [PATCH 76/77] cleanurl fix --- .../stirling/software/SPDF/config/CleanUrlInterceptor.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index bdcd183a2f..d37d4bfb61 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -45,13 +45,6 @@ public class CleanUrlInterceptor implements HandlerInterceptor { String queryString = request.getQueryString(); if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - - if (requestURI.contains("/api/")) { - return true; - } - - Map allowedParameters = new HashMap<>(); // Keep only the allowed parameters From adf6feea270c36145d71f84ac7e2352c59617da0 Mon Sep 17 00:00:00 2001 From: James Brunton Date: Tue, 12 Aug 2025 09:56:31 +0100 Subject: [PATCH 77/77] Neaten garbage collection code (#4171) # Description of Changes Slight tweaks to type checking code merged in #4126. Co-authored-by: James --- frontend/src/contexts/FileContext.tsx | 15 ++++++--------- .../src/services/enhancedPDFProcessingService.ts | 5 ++--- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index c49a43e389..953c7720fe 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -460,9 +460,8 @@ export function FileContextProvider({ thumbnailGenerationService.destroy(); // Force garbage collection hint - if (typeof window !== 'undefined' && window.gc) { - let gc = window.gc - setTimeout(() => gc(), 100); + if (typeof window !== 'undefined') { + setTimeout(() => window.gc && window.gc(), 100); } } catch (error) { @@ -597,10 +596,9 @@ export function FileContextProvider({ dispatch({ type: 'SET_CURRENT_MODE', payload: mode }); if (state.currentMode !== mode && state.activeFiles.length > 0) { - if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) { - let gc = window.gc; + if (window.requestIdleCallback && typeof window !== 'undefined') { window.requestIdleCallback(() => { - gc(); + window.gc && window.gc(); }, { timeout: 5000 }); } } @@ -612,10 +610,9 @@ export function FileContextProvider({ dispatch({ type: 'SET_CURRENT_VIEW', payload: view }); if (state.currentView !== view && state.activeFiles.length > 0) { - if (window.requestIdleCallback && typeof window !== 'undefined' && window.gc) { - let gc = window.gc; + if (window.requestIdleCallback && typeof window !== 'undefined') { window.requestIdleCallback(() => { - gc(); + window.gc && window.gc(); }, { timeout: 5000 }); } } diff --git a/frontend/src/services/enhancedPDFProcessingService.ts b/frontend/src/services/enhancedPDFProcessingService.ts index aa7802084b..64bf69b8aa 100644 --- a/frontend/src/services/enhancedPDFProcessingService.ts +++ b/frontend/src/services/enhancedPDFProcessingService.ts @@ -519,9 +519,8 @@ export class EnhancedPDFProcessingService { this.notifyListeners(); // Force memory cleanup hint - if (typeof window !== 'undefined' && window.gc) { - let gc = window.gc; - setTimeout(() => gc(), 100); + if (typeof window !== 'undefined') { + setTimeout(() => window.gc && window.gc(), 100); } }