mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge branch 'main' into issue-397
This commit is contained in:
commit
be39e097fd
@ -39,7 +39,7 @@ jobs:
|
|||||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -361,7 +361,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/PR-Demo-cleanup.yml
vendored
2
.github/workflows/PR-Demo-cleanup.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/ai_pr_title_review.yml
vendored
2
.github/workflows/ai_pr_title_review.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/auto-labelerV2.yml
vendored
2
.github/workflows/auto-labelerV2.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
14
.github/workflows/build.yml
vendored
14
.github/workflows/build.yml
vendored
@ -56,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -143,7 +143,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -176,7 +176,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -225,7 +225,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ jobs:
|
|||||||
|
|
||||||
test-build-docker-images:
|
test-build-docker-images:
|
||||||
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||||
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
|
needs: [files-changed, build]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@ -274,7 +274,7 @@ jobs:
|
|||||||
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
|
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -299,7 +299,7 @@ jobs:
|
|||||||
STIRLING_PDF_DESKTOP_UI: false
|
STIRLING_PDF_DESKTOP_UI: false
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
|
|||||||
2
.github/workflows/check_properties.yml
vendored
2
.github/workflows/check_properties.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
pull-requests: write # Allow writing to pull requests
|
pull-requests: write # Allow writing to pull requests
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/licenses-update.yml
vendored
2
.github/workflows/licenses-update.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
repository-projects: write # Required for enabling automerge
|
repository-projects: write # Required for enabling automerge
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/manage-label.yml
vendored
2
.github/workflows/manage-label.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/multiOSReleases.yml
vendored
12
.github/workflows/multiOSReleases.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -148,7 +148,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -238,7 +238,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -301,7 +301,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/pre_commit.yml
vendored
2
.github/workflows/pre_commit.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/push-docker.yml
vendored
10
.github/workflows/push-docker.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -80,7 +80,7 @@ jobs:
|
|||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Convert repository owner to lowercase
|
||||||
id: repoowner
|
id: repoowner
|
||||||
@ -88,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Generate tags ultra-lite
|
||||||
id: meta2
|
id: meta2
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@ -165,7 +165,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
|||||||
6
.github/workflows/releaseArtifacts.yml
vendored
6
.github/workflows/releaseArtifacts.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/sync_files.yml
vendored
2
.github/workflows/sync_files.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/testdriver.yml
vendored
6
.github/workflows/testdriver.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
30
Dockerfile
30
Dockerfile
@ -38,11 +38,14 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
|||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN apk add --no-cache bash \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
&& ln -sf /bin/bash /bin/sh \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
&& printf '%s\n' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/community' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \
|
||||||
|
> /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -65,19 +68,23 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
tesseract-ocr-data-chi_sim \
|
tesseract-ocr-data-chi_sim \
|
||||||
tesseract-ocr-data-deu \
|
tesseract-ocr-data-deu \
|
||||||
tesseract-ocr-data-fra \
|
tesseract-ocr-data-fra \
|
||||||
tesseract-ocr-data-por \
|
tesseract-ocr-data-por \
|
||||||
unpaper \
|
unpaper \
|
||||||
# CV
|
# CV / Python
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
python3 \
|
python3 \
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
py3-pillow@testing \
|
py3-pillow \
|
||||||
py3-pdf2image@testing \
|
py3-pdf2image \
|
||||||
|
# Calibre
|
||||||
|
calibre \
|
||||||
# URW Base 35 fonts for better PDF rendering
|
# URW Base 35 fonts for better PDF rendering
|
||||||
font-urw-base35 && \
|
font-urw-base35 && \
|
||||||
|
# Calibre fixes
|
||||||
|
apk fix --no-cache calibre && \
|
||||||
python3 -m venv /opt/venv && \
|
python3 -m venv /opt/venv && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
@ -93,7 +100,8 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
ln -sf /bin/busybox /bin/sh
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,12 @@ RUN apt-get update && apt-get install -y \
|
|||||||
python3-venv \
|
python3-venv \
|
||||||
# ss -tln
|
# ss -tln
|
||||||
iproute2 \
|
iproute2 \
|
||||||
|
# calibre requires these dependencies
|
||||||
|
wget \
|
||||||
|
xz-utils \
|
||||||
|
libopengl0 \
|
||||||
|
libxcb-cursor0 \
|
||||||
|
&& wget -nv -O- https://download.calibre-ebook.com/linux-installer.sh | sh /dev/stdin \
|
||||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Setze die Environment Variable für setuptools
|
# Setze die Environment Variable für setuptools
|
||||||
@ -38,7 +44,7 @@ ENV SETUPTOOLS_USE_DISTUTILS=local \
|
|||||||
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
||||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||||
&& . /opt/venv/bin/activate \
|
&& . /opt/venv/bin/activate \
|
||||||
&& pip install --no-cache-dir --require-hashes -r /tmp/requirements_dev.txt
|
&& pip install --no-cache-dir --only-binary=:all: --require-hashes -r /tmp/requirements_dev.txt
|
||||||
|
|
||||||
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||||
ENV PATH="/opt/venv/bin:$PATH"
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|||||||
@ -17,9 +17,9 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
# Build the application with DISABLE_ADDITIONAL_FEATURES=false
|
||||||
RUN DISABLE_ADDITIONAL_FEATURES=false \
|
ENV DISABLE_ADDITIONAL_FEATURES=false \
|
||||||
STIRLING_PDF_DESKTOP_UI=false \
|
STIRLING_PDF_DESKTOP_UI=false
|
||||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
RUN ./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
|
FROM alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412
|
||||||
@ -52,11 +52,14 @@ ENV DISABLE_ADDITIONAL_FEATURES=true \
|
|||||||
TEMP=/tmp/stirling-pdf \
|
TEMP=/tmp/stirling-pdf \
|
||||||
TMP=/tmp/stirling-pdf
|
TMP=/tmp/stirling-pdf
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN apk add --no-cache bash \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
&& ln -sf /bin/bash /bin/sh \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
&& printf '%s\n' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/community' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \
|
||||||
|
> /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -79,18 +82,22 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
tesseract-ocr-data-chi_sim \
|
tesseract-ocr-data-chi_sim \
|
||||||
tesseract-ocr-data-deu \
|
tesseract-ocr-data-deu \
|
||||||
tesseract-ocr-data-fra \
|
tesseract-ocr-data-fra \
|
||||||
tesseract-ocr-data-por \
|
tesseract-ocr-data-por \
|
||||||
unpaper \
|
unpaper \
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine font-urw-base35 \
|
||||||
# CV
|
# CV / Python
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
python3 \
|
python3 \
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
py3-pillow@testing \
|
py3-pillow \
|
||||||
py3-pdf2image@testing && \
|
py3-pdf2image \
|
||||||
|
# Calibre (musl-native) + QtWebEngine Runtime
|
||||||
|
calibre && \
|
||||||
|
# Calibre fixes
|
||||||
|
apk fix --no-cache calibre && \
|
||||||
python3 -m venv /opt/venv && \
|
python3 -m venv /opt/venv && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade pip setuptools && \
|
||||||
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
||||||
@ -106,7 +113,8 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /tmp/stirling-pdf && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
ln -sf /bin/busybox /bin/sh
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
|
|||||||
@ -24,9 +24,13 @@ COPY scripts/installFonts.sh /scripts/installFonts.sh
|
|||||||
COPY app/core/build/libs/*.jar app.jar
|
COPY app/core/build/libs/*.jar app.jar
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN apk add --no-cache bash \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
&& ln -sf /bin/bash /bin/sh \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
&& printf '%s\n' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/main' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/community' \
|
||||||
|
'https://dl-cdn.alpinelinux.org/alpine/edge/testing' \
|
||||||
|
> /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
@ -42,7 +46,8 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
chmod +x /scripts/*.sh && \
|
chmod +x /scripts/*.sh && \
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /tmp/stirling-pdf && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /pipeline /configs /customFiles /tmp/stirling-pdf && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
ln -sf /bin/busybox /bin/sh
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|||||||
@ -52,6 +52,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
|||||||
- **CBZ to PDF**: Convert comic book archives
|
- **CBZ to PDF**: Convert comic book archives
|
||||||
- **CBR to PDF**: Convert comic book rar archives
|
- **CBR to PDF**: Convert comic book rar archives
|
||||||
- **Email to PDF**: Convert email files to PDF
|
- **Email to PDF**: Convert email files to PDF
|
||||||
|
- **eBook to PDF**: Convert eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF (using Calibre)
|
||||||
- **Vector Image to PDF**: Convert vector images (PS, EPS, EPSF) to PDF format
|
- **Vector Image to PDF**: Convert vector images (PS, EPS, EPSF) to PDF format
|
||||||
|
|
||||||
#### Convert from PDF
|
#### Convert from PDF
|
||||||
|
|||||||
@ -70,7 +70,7 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "loginEnabled")
|
@Bean(name = "loginEnabled")
|
||||||
public boolean loginEnabled() {
|
public boolean loginEnabled() {
|
||||||
return applicationProperties.getSecurity().getEnableLogin();
|
return applicationProperties.getSecurity().isEnableLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "appName")
|
@Bean(name = "appName")
|
||||||
@ -120,9 +120,7 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "enableAlphaFunctionality")
|
@Bean(name = "enableAlphaFunctionality")
|
||||||
public boolean enableAlphaFunctionality() {
|
public boolean enableAlphaFunctionality() {
|
||||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
return applicationProperties.getSystem().isEnableAlphaFunctionality();
|
||||||
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "rateLimit")
|
@Bean(name = "rateLimit")
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public class RuntimePathConfig {
|
|||||||
private final String basePath;
|
private final String basePath;
|
||||||
private final String weasyPrintPath;
|
private final String weasyPrintPath;
|
||||||
private final String unoConvertPath;
|
private final String unoConvertPath;
|
||||||
|
private final String calibrePath;
|
||||||
|
|
||||||
// Pipeline paths
|
// Pipeline paths
|
||||||
private final String pipelineWatchedFoldersPath;
|
private final String pipelineWatchedFoldersPath;
|
||||||
@ -57,6 +58,7 @@ public class RuntimePathConfig {
|
|||||||
// Initialize Operation paths
|
// Initialize Operation paths
|
||||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||||
|
String defaultCalibrePath = isDocker ? "/usr/bin/ebook-convert" : "ebook-convert";
|
||||||
|
|
||||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
||||||
this.weasyPrintPath =
|
this.weasyPrintPath =
|
||||||
@ -67,6 +69,9 @@ public class RuntimePathConfig {
|
|||||||
resolvePath(
|
resolvePath(
|
||||||
defaultUnoConvertPath,
|
defaultUnoConvertPath,
|
||||||
operations != null ? operations.getUnoconvert() : null);
|
operations != null ? operations.getUnoconvert() : null);
|
||||||
|
this.calibrePath =
|
||||||
|
resolvePath(
|
||||||
|
defaultCalibrePath, operations != null ? operations.getCalibre() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolvePath(String defaultPath, String customPath) {
|
private String resolvePath(String defaultPath, String customPath) {
|
||||||
|
|||||||
@ -112,8 +112,8 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Security {
|
public static class Security {
|
||||||
private Boolean enableLogin;
|
private boolean enableLogin;
|
||||||
private Boolean csrfDisabled;
|
private boolean csrfDisabled;
|
||||||
private InitialLogin initialLogin = new InitialLogin();
|
private InitialLogin initialLogin = new InitialLogin();
|
||||||
private OAUTH2 oauth2 = new OAUTH2();
|
private OAUTH2 oauth2 = new OAUTH2();
|
||||||
private SAML2 saml2 = new SAML2();
|
private SAML2 saml2 = new SAML2();
|
||||||
@ -295,8 +295,8 @@ public class ApplicationProperties {
|
|||||||
throw new UnsupportedProviderException(
|
throw new UnsupportedProviderException(
|
||||||
"Logout from the provider "
|
"Logout from the provider "
|
||||||
+ registrationId
|
+ registrationId
|
||||||
+ " is not supported. "
|
+ " is not supported. Report it at"
|
||||||
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -314,19 +314,19 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class System {
|
public static class System {
|
||||||
private String defaultLocale;
|
private String defaultLocale;
|
||||||
private Boolean googlevisibility;
|
private boolean googlevisibility;
|
||||||
private boolean showUpdate;
|
private boolean showUpdate;
|
||||||
private Boolean showUpdateOnlyAdmin;
|
private boolean showUpdateOnlyAdmin;
|
||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
private String tessdataDir;
|
private String tessdataDir;
|
||||||
private Boolean enableAlphaFunctionality;
|
private boolean enableAlphaFunctionality;
|
||||||
private Boolean enableAnalytics;
|
private Boolean enableAnalytics;
|
||||||
private Boolean enablePosthog;
|
private Boolean enablePosthog;
|
||||||
private Boolean enableScarf;
|
private Boolean enableScarf;
|
||||||
private Datasource datasource;
|
private Datasource datasource;
|
||||||
private Boolean disableSanitize;
|
private boolean disableSanitize;
|
||||||
private int maxDPI;
|
private int maxDPI;
|
||||||
private Boolean enableUrlToPDF;
|
private boolean enableUrlToPDF;
|
||||||
private Html html = new Html();
|
private Html html = new Html();
|
||||||
private CustomPaths customPaths = new CustomPaths();
|
private CustomPaths customPaths = new CustomPaths();
|
||||||
private String fileUploadLimit;
|
private String fileUploadLimit;
|
||||||
@ -371,6 +371,7 @@ public class ApplicationProperties {
|
|||||||
public static class Operations {
|
public static class Operations {
|
||||||
private String weasyprint;
|
private String weasyprint;
|
||||||
private String unoconvert;
|
private String unoconvert;
|
||||||
|
private String calibre;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -453,10 +454,10 @@ public class ApplicationProperties {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return """
|
return """
|
||||||
Driver {
|
Driver {
|
||||||
driverName='%s'
|
driverName='%s'
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
.formatted(driverName);
|
.formatted(driverName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -491,7 +492,7 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Metrics {
|
public static class Metrics {
|
||||||
private Boolean enabled;
|
private boolean enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@ -253,11 +253,11 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"security_enableLogin",
|
"security_enableLogin",
|
||||||
applicationProperties.getSecurity().getEnableLogin());
|
applicationProperties.getSecurity().isEnableLogin());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"security_csrfDisabled",
|
"security_csrfDisabled",
|
||||||
applicationProperties.getSecurity().getCsrfDisabled());
|
applicationProperties.getSecurity().isCsrfDisabled());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"security_loginAttemptCount",
|
"security_loginAttemptCount",
|
||||||
@ -302,13 +302,13 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_googlevisibility",
|
"system_googlevisibility",
|
||||||
applicationProperties.getSystem().getGooglevisibility());
|
applicationProperties.getSystem().isGooglevisibility());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_showUpdateOnlyAdmin",
|
"system_showUpdateOnlyAdmin",
|
||||||
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
|
applicationProperties.getSystem().isShowUpdateOnlyAdmin());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_customHTMLFiles",
|
"system_customHTMLFiles",
|
||||||
@ -320,7 +320,7 @@ public class PostHogService {
|
|||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_enableAlphaFunctionality",
|
"system_enableAlphaFunctionality",
|
||||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
applicationProperties.getSystem().isEnableAlphaFunctionality());
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties,
|
properties,
|
||||||
"system_enableAnalytics",
|
"system_enableAnalytics",
|
||||||
@ -337,7 +337,7 @@ public class PostHogService {
|
|||||||
|
|
||||||
// Capture Metrics properties
|
// Capture Metrics properties
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
|
properties, "metrics_enabled", applicationProperties.getMetrics().isEnabled());
|
||||||
|
|
||||||
// Capture EnterpriseEdition properties
|
// Capture EnterpriseEdition properties
|
||||||
addIfNotEmpty(
|
addIfNotEmpty(
|
||||||
|
|||||||
@ -62,8 +62,7 @@ public class CustomHtmlSanitizer {
|
|||||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||||
|
|
||||||
public String sanitize(String html) {
|
public String sanitize(String html) {
|
||||||
boolean disableSanitize =
|
boolean disableSanitize = applicationProperties.getSystem().isDisableSanitize();
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
return disableSanitize ? html : POLICY.sanitize(html);
|
return disableSanitize ? html : POLICY.sanitize(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import org.junit.jupiter.params.provider.CsvSource;
|
|||||||
|
|
||||||
public class FileInfoTest {
|
public class FileInfoTest {
|
||||||
|
|
||||||
|
private static final LocalDateTime FIXED_NOW = LocalDateTime.of(2025, 11, 1, 12, 0, 0);
|
||||||
|
|
||||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||||
@CsvSource({
|
@CsvSource({
|
||||||
"0, '0 Bytes'",
|
"0, '0 Bytes'",
|
||||||
@ -28,9 +30,9 @@ public class FileInfoTest {
|
|||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
LocalDateTime.now(),
|
FIXED_NOW,
|
||||||
fileSize,
|
fileSize,
|
||||||
LocalDateTime.now().minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||||
}
|
}
|
||||||
@ -45,9 +47,9 @@ public class FileInfoTest {
|
|||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
LocalDateTime.now(),
|
FIXED_NOW,
|
||||||
123,
|
123,
|
||||||
LocalDateTime.now().minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
Path path = fi.getFilePathAsPath();
|
Path path = fi.getFilePathAsPath();
|
||||||
|
|
||||||
@ -103,7 +105,7 @@ public class FileInfoTest {
|
|||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
null, // modificationDate null
|
null, // modificationDate null
|
||||||
1,
|
1,
|
||||||
LocalDateTime.now());
|
FIXED_NOW);
|
||||||
|
|
||||||
assertThrows(
|
assertThrows(
|
||||||
NullPointerException.class,
|
NullPointerException.class,
|
||||||
@ -120,7 +122,7 @@ public class FileInfoTest {
|
|||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
LocalDateTime.now(),
|
FIXED_NOW,
|
||||||
1,
|
1,
|
||||||
null); // creationDate null
|
null); // creationDate null
|
||||||
|
|
||||||
@ -142,9 +144,9 @@ public class FileInfoTest {
|
|||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
LocalDateTime.now(),
|
FIXED_NOW,
|
||||||
1536, // 1.5 KB
|
1536, // 1.5 KB
|
||||||
LocalDateTime.now().minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
assertEquals("1.50 KB", fi.getFormattedFileSize());
|
assertEquals("1.50 KB", fi.getFormattedFileSize());
|
||||||
}
|
}
|
||||||
@ -158,9 +160,9 @@ public class FileInfoTest {
|
|||||||
new FileInfo(
|
new FileInfo(
|
||||||
"example.txt",
|
"example.txt",
|
||||||
"/path/to/example.txt",
|
"/path/to/example.txt",
|
||||||
LocalDateTime.now(),
|
FIXED_NOW,
|
||||||
twoTB,
|
twoTB,
|
||||||
LocalDateTime.now().minusDays(1));
|
FIXED_NOW.minusDays(1));
|
||||||
|
|
||||||
// 2 TB equals 2048.00 GB with current implementation
|
// 2 TB equals 2048.00 GB with current implementation
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import static org.mockito.Mockito.*;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -83,7 +82,7 @@ class FileStorageTest {
|
|||||||
void testRetrieveFile() throws IOException {
|
void testRetrieveFile() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-1";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@ -103,7 +102,7 @@ class FileStorageTest {
|
|||||||
void testRetrieveBytes() throws IOException {
|
void testRetrieveBytes() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-2";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@ -136,7 +135,7 @@ class FileStorageTest {
|
|||||||
void testDeleteFile() throws IOException {
|
void testDeleteFile() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-3";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
@ -164,7 +163,7 @@ class FileStorageTest {
|
|||||||
void testFileExists() throws IOException {
|
void testFileExists() throws IOException {
|
||||||
// Arrange
|
// Arrange
|
||||||
byte[] fileContent = "Test PDF content".getBytes();
|
byte[] fileContent = "Test PDF content".getBytes();
|
||||||
String fileId = UUID.randomUUID().toString();
|
String fileId = "test-file-4";
|
||||||
Path filePath = tempDir.resolve(fileId);
|
Path filePath = tempDir.resolve(fileId);
|
||||||
Files.write(filePath, fileContent);
|
Files.write(filePath, fileContent);
|
||||||
|
|
||||||
|
|||||||
@ -166,13 +166,13 @@ class JobExecutorServiceTest {
|
|||||||
// Given
|
// Given
|
||||||
Supplier<Object> work =
|
Supplier<Object> work =
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
// Simulate long-running job without actual sleep
|
||||||
Thread.sleep(100); // Simulate long-running job
|
// Use a loop to consume time instead of Thread.sleep
|
||||||
return "test-result";
|
long startTime = System.nanoTime();
|
||||||
} catch (InterruptedException e) {
|
while (System.nanoTime() - startTime < 100_000_000) { // 100ms in nanoseconds
|
||||||
Thread.currentThread().interrupt();
|
// Busy wait to simulate work without Thread.sleep
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
|
return "test-result";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
// Use reflection to access the private executeWithTimeout method
|
||||||
|
|||||||
@ -126,12 +126,15 @@ class ResourceMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resourceMetricsShouldDetectStaleState() {
|
void resourceMetricsShouldDetectStaleState() {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Given
|
// Given
|
||||||
Instant now = Instant.now();
|
Instant pastInstant =
|
||||||
Instant pastInstant = now.minusMillis(6000);
|
testTime.minusMillis(6000); // 6 seconds ago (relative to test start time)
|
||||||
|
|
||||||
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
||||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, testTime);
|
||||||
|
|
||||||
// When/Then
|
// When/Then
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import static org.mockito.Mockito.*;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -42,7 +41,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testCreateTask() {
|
void testCreateTask() {
|
||||||
// Act
|
// Act
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-1";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@ -56,7 +55,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetResult() {
|
void testSetResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-2";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
Object resultObject = "Test result";
|
Object resultObject = "Test result";
|
||||||
|
|
||||||
@ -74,7 +73,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetFileResult() throws Exception {
|
void testSetFileResult() throws Exception {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-3";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String fileId = "file-id";
|
String fileId = "file-id";
|
||||||
String originalFileName = "test.pdf";
|
String originalFileName = "test.pdf";
|
||||||
@ -108,7 +107,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetError() {
|
void testSetError() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-4";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String errorMessage = "Test error";
|
String errorMessage = "Test error";
|
||||||
|
|
||||||
@ -126,7 +125,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetComplete_WithExistingResult() {
|
void testSetComplete_WithExistingResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-5";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
Object resultObject = "Test result";
|
Object resultObject = "Test result";
|
||||||
taskManager.setResult(jobId, resultObject);
|
taskManager.setResult(jobId, resultObject);
|
||||||
@ -144,7 +143,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSetComplete_WithoutExistingResult() {
|
void testSetComplete_WithoutExistingResult() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-6";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -160,7 +159,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testIsComplete() {
|
void testIsComplete() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-7";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
|
|
||||||
// Assert - not complete initially
|
// Assert - not complete initially
|
||||||
@ -216,6 +215,8 @@ class TaskManagerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCleanupOldJobs() {
|
void testCleanupOldJobs() {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final LocalDateTime testTime = LocalDateTime.now();
|
||||||
// Arrange
|
// Arrange
|
||||||
// 1. Create a recent completed job
|
// 1. Create a recent completed job
|
||||||
String recentJobId = "recent-job";
|
String recentJobId = "recent-job";
|
||||||
@ -227,8 +228,9 @@ class TaskManagerTest {
|
|||||||
taskManager.createTask(oldJobId);
|
taskManager.createTask(oldJobId);
|
||||||
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||||
|
|
||||||
// Manually set the completion time to be older than the expiry
|
// Manually set the completion time to be older than the expiry (relative to test start
|
||||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
// time)
|
||||||
|
LocalDateTime oldTime = testTime.minusHours(1);
|
||||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
|
|
||||||
@ -280,7 +282,7 @@ class TaskManagerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testAddNote() {
|
void testAddNote() {
|
||||||
// Arrange
|
// Arrange
|
||||||
String jobId = UUID.randomUUID().toString();
|
String jobId = "test-job-8";
|
||||||
taskManager.createTask(jobId);
|
taskManager.createTask(jobId);
|
||||||
String note = "Test note";
|
String note = "Test note";
|
||||||
|
|
||||||
|
|||||||
@ -131,6 +131,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for each directory we'll process
|
// Mock Files.list for each directory we'll process
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@ -175,18 +178,17 @@ public class TempFileCleanupServiceTest {
|
|||||||
// maxAgeMillis
|
// maxAgeMillis
|
||||||
if (fileName.contains("old")) {
|
if (fileName.contains("old")) {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 5000000);
|
testTime - 5000000); // ~1.4 hours ago
|
||||||
}
|
}
|
||||||
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
||||||
// empty file test)
|
// empty file test)
|
||||||
else if (fileName.equals("empty.tmp")) {
|
else if ("empty.tmp".equals(fileName)) {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||||
}
|
}
|
||||||
// For all other files, return a recent timestamp
|
// For all other files, return a recent timestamp
|
||||||
else {
|
else {
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||||
System.currentTimeMillis() - 60000); // 1 minute ago
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -199,7 +201,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
// Return 0 bytes for the empty file
|
// Return 0 bytes for the empty file
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
// Return normal size for all other files
|
// Return normal size for all other files
|
||||||
@ -274,6 +276,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@ -288,9 +293,7 @@ public class TempFileCleanupServiceTest {
|
|||||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
.thenReturn(
|
.thenReturn(FileTime.fromMillis(testTime - 60000)); // 1 minute ago
|
||||||
FileTime.fromMillis(
|
|
||||||
System.currentTimeMillis() - 60000)); // 1 minute ago
|
|
||||||
|
|
||||||
// Configure Files.size to return normal size
|
// Configure Files.size to return normal size
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
||||||
@ -335,6 +338,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.list(eq(systemTempDir)))
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
@ -354,14 +360,14 @@ public class TempFileCleanupServiceTest {
|
|||||||
Path path = invocation.getArgument(0);
|
Path path = invocation.getArgument(0);
|
||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.equals("empty.tmp")) {
|
if ("empty.tmp".equals(fileName)) {
|
||||||
// More than 5 minutes old
|
// More than 5 minutes old
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||||
} else {
|
} else {
|
||||||
// Less than 5 minutes old
|
// Less than 5 minutes old
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(
|
||||||
System.currentTimeMillis() - 2 * 60 * 1000);
|
testTime - 2 * 60 * 1000); // 2 minutes ago
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -410,14 +416,25 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Use MockedStatic to mock Files operations
|
// Use MockedStatic to mock Files operations
|
||||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final long testTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Mock Files.list for each directory
|
// Mock Files.list for each directory
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir))).thenReturn(Stream.of(dir1));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(dir1));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir1))).thenReturn(Stream.of(tempFile1, dir2));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir1)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile1, dir2));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir2))).thenReturn(Stream.of(tempFile2, dir3));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir2)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile2, dir3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir3))).thenReturn(Stream.of(tempFile3));
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(dir3)))
|
||||||
|
.thenAnswer(invocation -> Stream.of(tempFile3));
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||||
@ -430,6 +447,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
// Configure Files.exists to return true for all paths
|
// Configure Files.exists to return true for all paths
|
||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
|
// Configure Files.size to return 0 for all files (ensure they're not empty)
|
||||||
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
@ -439,19 +459,14 @@ public class TempFileCleanupServiceTest {
|
|||||||
String fileName = path.getFileName().toString();
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.contains("old")) {
|
if (fileName.contains("old")) {
|
||||||
// Old file
|
// Old file - very old timestamp (older than 1 hour)
|
||||||
return FileTime.fromMillis(
|
return FileTime.fromMillis(testTime - 7200000); // 2 hours ago
|
||||||
System.currentTimeMillis() - 5000000);
|
|
||||||
} else {
|
} else {
|
||||||
// Recent file
|
// Recent file - very recent timestamp (less than 1 hour)
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure Files.size to return normal size
|
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
|
||||||
mockedFiles
|
mockedFiles
|
||||||
.when(() -> Files.deleteIfExists(any(Path.class)))
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
.thenAnswer(
|
.thenAnswer(
|
||||||
@ -461,13 +476,9 @@ public class TempFileCleanupServiceTest {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Act
|
// Act - pass maxAgeMillis = 3600000 (1 hour)
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||||
|
|
||||||
// Debug - print what was deleted
|
|
||||||
System.out.println("Deleted files: " + deletedFiles);
|
|
||||||
System.out.println("Looking for: " + tempFile3);
|
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||||
assertFalse(deletedFiles.contains(tempFile2), "Recent temp file should be preserved");
|
assertFalse(deletedFiles.contains(tempFile2), "Recent temp file should be preserved");
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
// strict-stubbing failures when individual tests bypass certain branches.
|
// strict-stubbing failures when individual tests bypass certain branches.
|
||||||
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(true);
|
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(true);
|
||||||
lenient().when(applicationProperties.getSystem()).thenReturn(systemProperties);
|
lenient().when(applicationProperties.getSystem()).thenReturn(systemProperties);
|
||||||
lenient().when(systemProperties.getDisableSanitize()).thenReturn(false);
|
lenient().when(systemProperties.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
customHtmlSanitizer = new CustomHtmlSanitizer(ssrfProtectionService, applicationProperties);
|
customHtmlSanitizer = new CustomHtmlSanitizer(ssrfProtectionService, applicationProperties);
|
||||||
}
|
}
|
||||||
@ -374,7 +374,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<p>ok</p><script>alert('XSS')</script><img src=\"http://blocked.local/a.png\">";
|
"<p>ok</p><script>alert('XSS')</script><img src=\"http://blocked.local/a.png\">";
|
||||||
|
|
||||||
// For this test, disable sanitize
|
// For this test, disable sanitize
|
||||||
when(systemProperties.getDisableSanitize()).thenReturn(true);
|
when(systemProperties.isDisableSanitize()).thenReturn(true);
|
||||||
|
|
||||||
// Also ensure SSRF would block it if sanitization were enabled (to prove bypass)
|
// Also ensure SSRF would block it if sanitization were enabled (to prove bypass)
|
||||||
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false);
|
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false);
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class EmlToPdfTest {
|
|||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
customHtmlSanitizer =
|
customHtmlSanitizer =
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
|
|||||||
@ -45,12 +45,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("test-file.txt");
|
Path testFile = tempDir.resolve("test-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
||||||
@ -58,12 +61,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("recent-file.txt");
|
Path testFile = tempDir.resolve("recent-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to just now
|
// Set modified time to just now (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now()));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime));
|
||||||
|
|
||||||
// File should not be ready for processing as it was just modified
|
// File should not be ready for processing as it was just modified
|
||||||
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
||||||
@ -80,12 +86,16 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("locked-file.txt");
|
Path testFile = tempDir.resolve("locked-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago to make sure it passes the time check
|
// Set modified time to 10 seconds ago (relative to test start time) to make sure it passes
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
// the time check
|
||||||
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// Verify the file is considered ready when it meets the time criteria
|
// Verify the file is considered ready when it meets the time criteria
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@ -104,12 +114,12 @@ class FileMonitorTest {
|
|||||||
// Create a PDF file
|
// Create a PDF file
|
||||||
Path pdfFile = tempDir.resolve("test.pdf");
|
Path pdfFile = tempDir.resolve("test.pdf");
|
||||||
Files.write(pdfFile, "pdf content".getBytes());
|
Files.write(pdfFile, "pdf content".getBytes());
|
||||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// Create a TXT file
|
// Create a TXT file
|
||||||
Path txtFile = tempDir.resolve("test.txt");
|
Path txtFile = tempDir.resolve("test.txt");
|
||||||
Files.write(txtFile, "text content".getBytes());
|
Files.write(txtFile, "text content".getBytes());
|
||||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// PDF file should be ready for processing
|
// PDF file should be ready for processing
|
||||||
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
||||||
@ -125,12 +135,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("in-use-file.txt");
|
Path testFile = tempDir.resolve("in-use-file.txt");
|
||||||
Files.write(testFile, "initial content".getBytes());
|
Files.write(testFile, "initial content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// First check that the file is ready when meeting time criteria
|
// First check that the file is ready when meeting time criteria
|
||||||
assertTrue(
|
assertTrue(
|
||||||
@ -139,7 +152,7 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
// After modifying the file to simulate closing, it should still be ready
|
// After modifying the file to simulate closing, it should still be ready
|
||||||
Files.write(testFile, "updated content".getBytes());
|
Files.write(testFile, "updated content".getBytes());
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
assertTrue(
|
assertTrue(
|
||||||
fileMonitor.isFileReadyForProcessing(testFile),
|
fileMonitor.isFileReadyForProcessing(testFile),
|
||||||
@ -148,12 +161,15 @@ class FileMonitorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
||||||
|
// Capture test time at the beginning for deterministic calculations
|
||||||
|
final Instant testTime = Instant.now();
|
||||||
|
|
||||||
// Create a test file
|
// Create a test file
|
||||||
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
||||||
Files.write(testFile, "test content".getBytes());
|
Files.write(testFile, "test content".getBytes());
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago (relative to test start time)
|
||||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||||
|
|
||||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||||
// Use the absolute path to make sure it's handled correctly
|
// Use the absolute path to make sure it's handled correctly
|
||||||
@ -167,7 +183,7 @@ class FileMonitorTest {
|
|||||||
Files.createDirectory(testDir);
|
Files.createDirectory(testDir);
|
||||||
|
|
||||||
// Set modified time to 10 seconds ago
|
// Set modified time to 10 seconds ago
|
||||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.now().minusMillis(10000)));
|
Files.setLastModifiedTime(testDir, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||||
|
|
||||||
// A directory should not be considered ready for processing
|
// A directory should not be considered ready for processing
|
||||||
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ public class FileToPdfTest {
|
|||||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
.thenReturn(true);
|
.thenReturn(true);
|
||||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
customHtmlSanitizer =
|
customHtmlSanitizer =
|
||||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
|
|||||||
@ -55,7 +55,7 @@ dependencies {
|
|||||||
implementation project(':common')
|
implementation project(':common')
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
||||||
implementation 'com.posthog.java:posthog:1.2.0'
|
implementation 'com.posthog.java:posthog:1.2.0'
|
||||||
implementation 'commons-io:commons-io:2.20.0'
|
implementation 'commons-io:commons-io:2.21.0'
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation 'io.micrometer:micrometer-core:1.15.5'
|
implementation 'io.micrometer:micrometer-core:1.15.5'
|
||||||
|
|||||||
@ -257,6 +257,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Convert", "html-to-pdf");
|
addEndpointToGroup("Convert", "html-to-pdf");
|
||||||
addEndpointToGroup("Convert", "url-to-pdf");
|
addEndpointToGroup("Convert", "url-to-pdf");
|
||||||
addEndpointToGroup("Convert", "markdown-to-pdf");
|
addEndpointToGroup("Convert", "markdown-to-pdf");
|
||||||
|
addEndpointToGroup("Convert", "ebook-to-pdf");
|
||||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||||
@ -446,6 +447,9 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Weasyprint", "markdown-to-pdf");
|
addEndpointToGroup("Weasyprint", "markdown-to-pdf");
|
||||||
addEndpointToGroup("Weasyprint", "eml-to-pdf");
|
addEndpointToGroup("Weasyprint", "eml-to-pdf");
|
||||||
|
|
||||||
|
// Calibre dependent endpoints
|
||||||
|
addEndpointToGroup("Calibre", "ebook-to-pdf");
|
||||||
|
|
||||||
// Pdftohtml dependent endpoints
|
// Pdftohtml dependent endpoints
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
||||||
@ -475,7 +479,7 @@ public class EndpointConfiguration {
|
|||||||
disableGroup("enterprise");
|
disableGroup("enterprise");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
|
||||||
disableEndpoint("url-to-pdf");
|
disableEndpoint("url-to-pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -498,6 +502,7 @@ public class EndpointConfiguration {
|
|||||||
|| "Javascript".equals(group)
|
|| "Javascript".equals(group)
|
||||||
|| "Weasyprint".equals(group)
|
|| "Weasyprint".equals(group)
|
||||||
|| "Pdftohtml".equals(group)
|
|| "Pdftohtml".equals(group)
|
||||||
|
|| "Calibre".equals(group)
|
||||||
|| "rar".equals(group)
|
|| "rar".equals(group)
|
||||||
|| "FFmpeg".equals(group);
|
|| "FFmpeg".equals(group);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ public class ExternalAppDepConfig {
|
|||||||
|
|
||||||
private final String weasyprintPath;
|
private final String weasyprintPath;
|
||||||
private final String unoconvPath;
|
private final String unoconvPath;
|
||||||
|
private final String calibrePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of command(binary) -> affected groups (e.g. "gs" -> ["Ghostscript"]). Immutable to avoid
|
* Map of command(binary) -> affected groups (e.g. "gs" -> ["Ghostscript"]). Immutable to avoid
|
||||||
@ -56,6 +57,7 @@ public class ExternalAppDepConfig {
|
|||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
this.weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
this.weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
||||||
this.unoconvPath = runtimePathConfig.getUnoConvertPath();
|
this.unoconvPath = runtimePathConfig.getUnoConvertPath();
|
||||||
|
this.calibrePath = runtimePathConfig.getCalibrePath();
|
||||||
|
|
||||||
Map<String, List<String>> tmp = new HashMap<>();
|
Map<String, List<String>> tmp = new HashMap<>();
|
||||||
tmp.put("gs", List.of("Ghostscript"));
|
tmp.put("gs", List.of("Ghostscript"));
|
||||||
@ -67,6 +69,7 @@ public class ExternalAppDepConfig {
|
|||||||
tmp.put("qpdf", List.of("qpdf"));
|
tmp.put("qpdf", List.of("qpdf"));
|
||||||
tmp.put("tesseract", List.of("tesseract"));
|
tmp.put("tesseract", List.of("tesseract"));
|
||||||
tmp.put("rar", List.of("rar"));
|
tmp.put("rar", List.of("rar"));
|
||||||
|
tmp.put(calibrePath, List.of("Calibre"));
|
||||||
tmp.put("ffmpeg", List.of("FFmpeg"));
|
tmp.put("ffmpeg", List.of("FFmpeg"));
|
||||||
this.commandToGroupMapping = Collections.unmodifiableMap(tmp);
|
this.commandToGroupMapping = Collections.unmodifiableMap(tmp);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,11 +61,9 @@ public class InitialSetup {
|
|||||||
public void initEnableCSRFSecurity() throws IOException {
|
public void initEnableCSRFSecurity() throws IOException {
|
||||||
if (GeneralUtils.isVersionHigher(
|
if (GeneralUtils.isVersionHigher(
|
||||||
"0.46.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
"0.46.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
||||||
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
boolean csrf = applicationProperties.getSecurity().isCsrfDisabled();
|
||||||
if (!csrf) {
|
if (!csrf) {
|
||||||
GeneralUtils.saveKeyToSettings("security.csrfDisabled", false);
|
|
||||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
|
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
|
||||||
applicationProperties.getSecurity().setCsrfDisabled(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ public class OpenApiConfig {
|
|||||||
.url("https://www.stirlingpdf.com")
|
.url("https://www.stirlingpdf.com")
|
||||||
.email("contact@stirlingpdf.com"))
|
.email("contact@stirlingpdf.com"))
|
||||||
.description(DEFAULT_DESCRIPTION);
|
.description(DEFAULT_DESCRIPTION);
|
||||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
if (!applicationProperties.getSecurity().isEnableLogin()) {
|
||||||
return new OpenAPI().components(new Components()).info(info);
|
return new OpenAPI().components(new Components()).info(info);
|
||||||
} else {
|
} else {
|
||||||
SecurityScheme apiKeyScheme =
|
SecurityScheme apiKeyScheme =
|
||||||
|
|||||||
@ -72,19 +72,29 @@ public class MergeController {
|
|||||||
// fileOrder is newline-delimited original filenames in the desired order.
|
// fileOrder is newline-delimited original filenames in the desired order.
|
||||||
private static MultipartFile[] reorderFilesByProvidedOrder(
|
private static MultipartFile[] reorderFilesByProvidedOrder(
|
||||||
MultipartFile[] files, String fileOrder) {
|
MultipartFile[] files, String fileOrder) {
|
||||||
String[] desired = fileOrder.split("\n", -1);
|
// Split by various line endings and trim each entry
|
||||||
|
String[] desired =
|
||||||
|
stirling.software.common.util.RegexPatternUtils.getInstance()
|
||||||
|
.getNewlineSplitPattern()
|
||||||
|
.split(fileOrder);
|
||||||
|
|
||||||
List<MultipartFile> remaining = new ArrayList<>(Arrays.asList(files));
|
List<MultipartFile> remaining = new ArrayList<>(Arrays.asList(files));
|
||||||
List<MultipartFile> ordered = new ArrayList<>(files.length);
|
List<MultipartFile> ordered = new ArrayList<>(files.length);
|
||||||
|
|
||||||
for (String name : desired) {
|
for (String name : desired) {
|
||||||
if (name == null || name.isEmpty()) continue;
|
name = name.trim();
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
log.debug("Skipping empty entry");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
int idx = indexOfByOriginalFilename(remaining, name);
|
int idx = indexOfByOriginalFilename(remaining, name);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
ordered.add(remaining.remove(idx));
|
ordered.add(remaining.remove(idx));
|
||||||
|
} else {
|
||||||
|
log.debug("Filename from order list not found in uploaded files: {}", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append any files not explicitly listed, preserving their relative order
|
|
||||||
ordered.addAll(remaining);
|
ordered.addAll(remaining);
|
||||||
return ordered.toArray(new MultipartFile[0]);
|
return ordered.toArray(new MultipartFile[0]);
|
||||||
}
|
}
|
||||||
@ -252,8 +262,10 @@ public class MergeController {
|
|||||||
|
|
||||||
// If front-end provided explicit visible order, honor it and override backend sorting
|
// If front-end provided explicit visible order, honor it and override backend sorting
|
||||||
if (fileOrder != null && !fileOrder.isBlank()) {
|
if (fileOrder != null && !fileOrder.isBlank()) {
|
||||||
|
log.info("Reordering files based on fileOrder parameter");
|
||||||
files = reorderFilesByProvidedOrder(files, fileOrder);
|
files = reorderFilesByProvidedOrder(files, fileOrder);
|
||||||
} else {
|
} else {
|
||||||
|
log.info("Sorting files based on sortType: {}", request.getSortType());
|
||||||
Arrays.sort(
|
Arrays.sort(
|
||||||
files,
|
files,
|
||||||
getSortComparator(
|
getSortComparator(
|
||||||
|
|||||||
@ -0,0 +1,208 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
|
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/convert")
|
||||||
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ConvertEbookToPDFController {
|
||||||
|
|
||||||
|
private static final Set<String> SUPPORTED_EXTENSIONS =
|
||||||
|
Set.of("epub", "mobi", "azw3", "fb2", "txt", "docx");
|
||||||
|
|
||||||
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final TempFileManager tempFileManager;
|
||||||
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
private boolean isCalibreEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("Calibre");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGhostscriptEnabled() {
|
||||||
|
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Convert an eBook file to PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)"
|
||||||
|
+ " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> convertEbookToPdf(
|
||||||
|
@ModelAttribute ConvertEbookToPdfRequest request) throws Exception {
|
||||||
|
if (!isCalibreEnabled()) {
|
||||||
|
throw new IllegalStateException("Calibre support is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
if (inputFile == null || inputFile.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("No input file provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean optimizeForEbook = Boolean.TRUE.equals(request.getOptimizeForEbook());
|
||||||
|
if (optimizeForEbook && !isGhostscriptEnabled()) {
|
||||||
|
log.warn(
|
||||||
|
"Ghostscript optimization requested but Ghostscript is not enabled/available"
|
||||||
|
+ " for ebook conversion");
|
||||||
|
optimizeForEbook = false;
|
||||||
|
}
|
||||||
|
boolean embedAllFonts = Boolean.TRUE.equals(request.getEmbedAllFonts());
|
||||||
|
boolean includeTableOfContents = Boolean.TRUE.equals(request.getIncludeTableOfContents());
|
||||||
|
boolean includePageNumbers = Boolean.TRUE.equals(request.getIncludePageNumbers());
|
||||||
|
|
||||||
|
String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||||
|
if (originalFilename == null || originalFilename.isBlank()) {
|
||||||
|
originalFilename = "document";
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = FilenameUtils.getExtension(originalFilename);
|
||||||
|
if (extension == null || extension.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("Unable to determine file type");
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerExtension = extension.toLowerCase(Locale.ROOT);
|
||||||
|
if (!SUPPORTED_EXTENSIONS.contains(lowerExtension)) {
|
||||||
|
throw new IllegalArgumentException("Unsupported eBook file extension: " + extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseName = FilenameUtils.getBaseName(originalFilename);
|
||||||
|
if (baseName == null || baseName.isBlank()) {
|
||||||
|
baseName = "document";
|
||||||
|
}
|
||||||
|
|
||||||
|
Path workingDirectory = tempFileManager.createTempDirectory();
|
||||||
|
Path inputPath = workingDirectory.resolve(baseName + "." + lowerExtension);
|
||||||
|
Path outputPath = workingDirectory.resolve(baseName + ".pdf");
|
||||||
|
|
||||||
|
try (InputStream inputStream = inputFile.getInputStream()) {
|
||||||
|
Files.copy(inputStream, inputPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> command =
|
||||||
|
buildCalibreCommand(
|
||||||
|
inputPath,
|
||||||
|
outputPath,
|
||||||
|
embedAllFonts,
|
||||||
|
includeTableOfContents,
|
||||||
|
includePageNumbers);
|
||||||
|
ProcessExecutorResult result =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
||||||
|
.runCommandWithOutputHandling(command, workingDirectory.toFile());
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
throw new IllegalStateException("Calibre conversion returned no result");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.getRc() != 0) {
|
||||||
|
String errorMessage = result.getMessages();
|
||||||
|
if (errorMessage == null || errorMessage.isBlank()) {
|
||||||
|
errorMessage = "Calibre conversion failed";
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Files.exists(outputPath) || Files.size(outputPath) == 0L) {
|
||||||
|
throw new IllegalStateException("Calibre did not produce a PDF output");
|
||||||
|
}
|
||||||
|
|
||||||
|
String outputFilename =
|
||||||
|
GeneralUtils.generateFilename(originalFilename, "_convertedToPDF.pdf");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (optimizeForEbook) {
|
||||||
|
byte[] pdfBytes = Files.readAllBytes(outputPath);
|
||||||
|
try {
|
||||||
|
byte[] optimizedPdf = GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(optimizedPdf, outputFilename);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn(
|
||||||
|
"Ghostscript optimization failed for ebook conversion, returning"
|
||||||
|
+ " original PDF",
|
||||||
|
e);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) {
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(document, outputFilename);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cleanupTempFiles(workingDirectory, inputPath, outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> buildCalibreCommand(
|
||||||
|
Path inputPath,
|
||||||
|
Path outputPath,
|
||||||
|
boolean embedAllFonts,
|
||||||
|
boolean includeTableOfContents,
|
||||||
|
boolean includePageNumbers) {
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add("ebook-convert");
|
||||||
|
command.add(inputPath.toString());
|
||||||
|
command.add(outputPath.toString());
|
||||||
|
|
||||||
|
if (embedAllFonts) {
|
||||||
|
command.add("--embed-all-fonts");
|
||||||
|
}
|
||||||
|
if (includeTableOfContents) {
|
||||||
|
command.add("--pdf-add-toc");
|
||||||
|
}
|
||||||
|
if (includePageNumbers) {
|
||||||
|
command.add("--pdf-page-numbers");
|
||||||
|
}
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupTempFiles(Path workingDirectory, Path inputPath, Path outputPath) {
|
||||||
|
List<Path> pathsToDelete = new ArrayList<>();
|
||||||
|
pathsToDelete.add(inputPath);
|
||||||
|
pathsToDelete.add(outputPath);
|
||||||
|
|
||||||
|
for (Path path : pathsToDelete) {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete temporary file: {}", path, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tempFileManager.deleteTempDirectory(workingDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -71,7 +71,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
URI location = null;
|
URI location = null;
|
||||||
HttpStatus status = HttpStatus.SEE_OTHER;
|
HttpStatus status = HttpStatus.SEE_OTHER;
|
||||||
|
|
||||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
|
||||||
location =
|
location =
|
||||||
uriComponentsBuilder
|
uriComponentsBuilder
|
||||||
.queryParam("error", "error.endpointDisabled")
|
.queryParam("error", "error.endpointDisabled")
|
||||||
|
|||||||
@ -47,6 +47,13 @@ public class ConverterWebController {
|
|||||||
return "convert/cbr-to-pdf";
|
return "convert/cbr-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ebook-to-pdf")
|
||||||
|
@Hidden
|
||||||
|
public String convertEbookToPdfForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "ebook-to-pdf");
|
||||||
|
return "convert/ebook-to-pdf";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-cbr")
|
@GetMapping("/pdf-to-cbr")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertPdfToCbrForm(Model model) {
|
public String convertPdfToCbrForm(Model model) {
|
||||||
|
|||||||
@ -84,8 +84,8 @@ public class HomeWebController {
|
|||||||
@ResponseBody
|
@ResponseBody
|
||||||
@Hidden
|
@Hidden
|
||||||
public String getRobotsTxt() {
|
public String getRobotsTxt() {
|
||||||
Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
|
boolean allowGoogle = applicationProperties.getSystem().isGooglevisibility();
|
||||||
if (Boolean.TRUE.equals(allowGoogle)) {
|
if (allowGoogle) {
|
||||||
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
|
||||||
} else {
|
} else {
|
||||||
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";
|
||||||
|
|||||||
@ -42,9 +42,7 @@ public class MetricsController {
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
|
metricsEnabled = applicationProperties.getMetrics().isEnabled();
|
||||||
if (metricsEnabled == null) metricsEnabled = true;
|
|
||||||
this.metricsEnabled = metricsEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/status")
|
@GetMapping("/status")
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
package stirling.software.SPDF.model.api.converters;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class ConvertEbookToPdfRequest {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The input eBook file to be converted to a PDF file (EPUB, MOBI, AZW3, FB2,"
|
||||||
|
+ " TXT, DOCX)",
|
||||||
|
contentMediaType =
|
||||||
|
"application/epub+zip, application/x-mobipocket-ebook, application/x-azw3,"
|
||||||
|
+ " text/xml, text/plain,"
|
||||||
|
+ " application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private MultipartFile fileInput;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Embed all fonts from the eBook into the generated PDF",
|
||||||
|
allowableValues = {"true", "false"},
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean embedAllFonts;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Add a generated table of contents to the resulting PDF",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
allowableValues = {"true", "false"},
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean includeTableOfContents;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Add page numbers to the generated PDF",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
allowableValues = {"true", "false"},
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean includePageNumbers;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Optimize the PDF for eBook reading (smaller file size, better rendering on"
|
||||||
|
+ " eInk devices)",
|
||||||
|
allowableValues = {"true", "false"},
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean optimizeForEbook;
|
||||||
|
}
|
||||||
@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR zu PDF
|
|||||||
home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren.
|
home.cbrToPdf.desc=CBR-Comicarchive in das PDF-Format konvertieren.
|
||||||
cbrToPdf.tags=konvertierung,comic,buch,archiv,cbr,rar
|
cbrToPdf.tags=konvertierung,comic,buch,archiv,cbr,rar
|
||||||
|
|
||||||
|
home.ebookToPdf.title=E-Book zu PDF
|
||||||
|
home.ebookToPdf.desc=E-Book-Dateien (EPUB, MOBI, AZW3, FB2, TXT, DOCX) mit Calibre in PDF konvertieren.
|
||||||
|
ebookToPdf.tags=konvertierung,ebook,calibre,epub,mobi,azw3
|
||||||
|
|
||||||
home.pdfToCbz.title=PDF zu CBZ
|
home.pdfToCbz.title=PDF zu CBZ
|
||||||
home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln.
|
home.pdfToCbz.desc=PDF-Dateien in CBZ-Comicarchive umwandeln.
|
||||||
pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf
|
pdfToCbz.tags=konvertierung,comic,buch,archiv,cbz,pdf
|
||||||
@ -1490,6 +1494,17 @@ cbrToPDF.submit=Zu PDF konvertieren
|
|||||||
cbrToPDF.selectText=CBR-Datei auswählen
|
cbrToPDF.selectText=CBR-Datei auswählen
|
||||||
cbrToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript)
|
cbrToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript)
|
||||||
|
|
||||||
|
#ebookToPDF
|
||||||
|
ebookToPDF.title=E-Book zu PDF
|
||||||
|
ebookToPDF.header=E-Book zu PDF
|
||||||
|
ebookToPDF.submit=Zu PDF konvertieren
|
||||||
|
ebookToPDF.selectText=E-Book-Datei auswählen
|
||||||
|
ebookToPDF.embedAllFonts=Alle Schriftarten in der erzeugten PDF einbetten (kann die Dateigröße erhöhen)
|
||||||
|
ebookToPDF.includeTableOfContents=Inhaltsverzeichnis zur erzeugten PDF hinzufügen
|
||||||
|
ebookToPDF.includePageNumbers=Seitenzahlen zur erzeugten PDF hinzufügen
|
||||||
|
ebookToPDF.optimizeForEbook=PDF für E-Book-Reader optimieren (verwendet Ghostscript)
|
||||||
|
ebookToPDF.calibreDisabled=Calibre-Unterstützung ist deaktiviert. Aktivieren Sie die Calibre-Werkzeuggruppe oder installieren Sie Calibre, um diese Funktion zu nutzen.
|
||||||
|
|
||||||
#pdfToCBR
|
#pdfToCBR
|
||||||
pdfToCBR.title=PDF zu CBR
|
pdfToCBR.title=PDF zu CBR
|
||||||
pdfToCBR.header=PDF zu CBR
|
pdfToCBR.header=PDF zu CBR
|
||||||
|
|||||||
@ -616,6 +616,10 @@ home.cbrToPdf.title=CBR to PDF
|
|||||||
home.cbrToPdf.desc=Convert CBR comic book archives to PDF format.
|
home.cbrToPdf.desc=Convert CBR comic book archives to PDF format.
|
||||||
cbrToPdf.tags=conversion,comic,book,archive,cbr,rar
|
cbrToPdf.tags=conversion,comic,book,archive,cbr,rar
|
||||||
|
|
||||||
|
home.ebookToPdf.title=eBook to PDF
|
||||||
|
home.ebookToPdf.desc=Convert eBook files (EPUB, MOBI, AZW3, FB2, TXT, DOCX) to PDF using Calibre.
|
||||||
|
ebookToPdf.tags=conversion,ebook,calibre,epub,mobi,azw3
|
||||||
|
|
||||||
home.pdfToCbz.title=PDF to CBZ
|
home.pdfToCbz.title=PDF to CBZ
|
||||||
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
|
home.pdfToCbz.desc=Convert PDF files to CBZ comic book archives.
|
||||||
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
|
pdfToCbz.tags=conversion,comic,book,archive,cbz,pdf
|
||||||
@ -1503,6 +1507,17 @@ cbrToPDF.submit=Convert to PDF
|
|||||||
cbrToPDF.selectText=Select CBR file
|
cbrToPDF.selectText=Select CBR file
|
||||||
cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
cbrToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
||||||
|
|
||||||
|
#ebookToPDF
|
||||||
|
ebookToPDF.title=eBook to PDF
|
||||||
|
ebookToPDF.header=eBook to PDF
|
||||||
|
ebookToPDF.submit=Convert to PDF
|
||||||
|
ebookToPDF.selectText=Select eBook file
|
||||||
|
ebookToPDF.embedAllFonts=Embed all fonts in the output PDF (may increase file size)
|
||||||
|
ebookToPDF.includeTableOfContents=Add a generated table of contents to the PDF
|
||||||
|
ebookToPDF.includePageNumbers=Add page numbers to the generated PDF
|
||||||
|
ebookToPDF.optimizeForEbook=Optimize PDF for ebook readers (uses Ghostscript)
|
||||||
|
ebookToPDF.calibreDisabled=Calibre support is disabled. Enable the Calibre tool group or install Calibre to use this feature.
|
||||||
|
|
||||||
#pdfToCBR
|
#pdfToCBR
|
||||||
pdfToCBR.title=PDF to CBR
|
pdfToCBR.title=PDF to CBR
|
||||||
pdfToCBR.header=PDF to CBR
|
pdfToCBR.header=PDF to CBR
|
||||||
|
|||||||
@ -149,6 +149,7 @@ system:
|
|||||||
operations:
|
operations:
|
||||||
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
weasyprint: '' # Defaults to /opt/venv/bin/weasyprint
|
||||||
unoconvert: '' # Defaults to /opt/venv/bin/unoconvert
|
unoconvert: '' # Defaults to /opt/venv/bin/unoconvert
|
||||||
|
calibre: '' # Defaults to /usr/bin/ebook-convert
|
||||||
fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB".
|
fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB".
|
||||||
tempFileManagement:
|
tempFileManagement:
|
||||||
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf
|
baseTmpDir: '' # Defaults to java.io.tmpdir/stirling-pdf
|
||||||
|
|||||||
@ -74,6 +74,12 @@
|
|||||||
showGameBtn.style.display = 'none';
|
showGameBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log fileOrder for debugging
|
||||||
|
const fileOrderValue = formData.get('fileOrder');
|
||||||
|
if (fileOrderValue) {
|
||||||
|
console.log('FormData fileOrder:', fileOrderValue);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove empty file entries
|
// Remove empty file entries
|
||||||
for (let [key, value] of formData.entries()) {
|
for (let [key, value] of formData.entries()) {
|
||||||
if (value instanceof File && !value.name) {
|
if (value instanceof File && !value.name) {
|
||||||
|
|||||||
@ -123,39 +123,38 @@ function attachMoveButtons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("sortByNameBtn").addEventListener("click", function () {
|
document.getElementById("sortByNameBtn").addEventListener("click", async function () {
|
||||||
if (currentSort.field === "name" && !currentSort.descending) {
|
if (currentSort.field === "name" && !currentSort.descending) {
|
||||||
currentSort.descending = true;
|
currentSort.descending = true;
|
||||||
sortFiles((a, b) => b.name.localeCompare(a.name));
|
await sortFiles((a, b) => b.name.localeCompare(a.name));
|
||||||
} else {
|
} else {
|
||||||
currentSort.field = "name";
|
currentSort.field = "name";
|
||||||
currentSort.descending = false;
|
currentSort.descending = false;
|
||||||
sortFiles((a, b) => a.name.localeCompare(b.name));
|
await sortFiles((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("sortByDateBtn").addEventListener("click", function () {
|
document.getElementById("sortByDateBtn").addEventListener("click", async function () {
|
||||||
if (currentSort.field === "lastModified" && !currentSort.descending) {
|
if (currentSort.field === "lastModified" && !currentSort.descending) {
|
||||||
currentSort.descending = true;
|
currentSort.descending = true;
|
||||||
sortFiles((a, b) => b.lastModified - a.lastModified);
|
await sortFiles((a, b) => b.lastModified - a.lastModified);
|
||||||
} else {
|
} else {
|
||||||
currentSort.field = "lastModified";
|
currentSort.field = "lastModified";
|
||||||
currentSort.descending = false;
|
currentSort.descending = false;
|
||||||
sortFiles((a, b) => a.lastModified - b.lastModified);
|
await sortFiles((a, b) => a.lastModified - b.lastModified);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function sortFiles(comparator) {
|
async function sortFiles(comparator) {
|
||||||
// Convert FileList to array and sort
|
// Convert FileList to array and sort
|
||||||
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
|
const sortedFilesArray = Array.from(document.getElementById("fileInput-input").files).sort(comparator);
|
||||||
|
|
||||||
// Refresh displayed list
|
// Refresh displayed list (wait for it to complete since it's async)
|
||||||
displayFiles(sortedFilesArray);
|
await displayFiles(sortedFilesArray);
|
||||||
|
|
||||||
// Update the files property
|
// Update the file input and fileOrder based on the current display order
|
||||||
const dataTransfer = new DataTransfer();
|
// This ensures consistency between display and file input
|
||||||
sortedFilesArray.forEach((file) => dataTransfer.items.add(file));
|
updateFiles();
|
||||||
document.getElementById("fileInput-input").files = dataTransfer.files;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFiles() {
|
function updateFiles() {
|
||||||
@ -163,25 +162,36 @@ function updateFiles() {
|
|||||||
var liElements = document.querySelectorAll("#selectedFiles li");
|
var liElements = document.querySelectorAll("#selectedFiles li");
|
||||||
const files = document.getElementById("fileInput-input").files;
|
const files = document.getElementById("fileInput-input").files;
|
||||||
|
|
||||||
|
console.log("updateFiles: found", liElements.length, "LI elements and", files.length, "files");
|
||||||
|
|
||||||
for (var i = 0; i < liElements.length; i++) {
|
for (var i = 0; i < liElements.length; i++) {
|
||||||
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
|
var fileNameFromList = liElements[i].querySelector(".filename").innerText;
|
||||||
var fileFromFiles;
|
var found = false;
|
||||||
for (var j = 0; j < files.length; j++) {
|
for (var j = 0; j < files.length; j++) {
|
||||||
var file = files[j];
|
var file = files[j];
|
||||||
if (file.name === fileNameFromList) {
|
if (file.name === fileNameFromList) {
|
||||||
dataTransfer.items.add(file);
|
dataTransfer.items.add(file);
|
||||||
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!found) {
|
||||||
|
console.warn("updateFiles: Could not find file:", fileNameFromList);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("fileInput-input").files = dataTransfer.files;
|
document.getElementById("fileInput-input").files = dataTransfer.files;
|
||||||
|
console.log("updateFiles: Updated file input with", dataTransfer.files.length, "files");
|
||||||
|
|
||||||
// Also populate hidden fileOrder to preserve visible order
|
// Also populate hidden fileOrder to preserve visible order
|
||||||
const order = Array.from(liElements)
|
const order = Array.from(liElements)
|
||||||
.map((li) => li.querySelector(".filename").innerText)
|
.map((li) => li.querySelector(".filename").innerText)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const orderInput = document.getElementById("fileOrder");
|
const orderInput = document.getElementById("fileOrder");
|
||||||
if (orderInput) orderInput.value = order;
|
if (orderInput) {
|
||||||
|
orderInput.value = order;
|
||||||
|
console.log("Updated fileOrder:", order);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{
|
document.querySelector("#resetFileInputBtn").addEventListener("click", ()=>{
|
||||||
|
|||||||
107
app/core/src/main/resources/templates/convert/ebook-to-pdf.html
Normal file
107
app/core/src/main/resources/templates/convert/ebook-to-pdf.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:data-language="${#locale.toString()}"
|
||||||
|
th:dir="#{language.direction}"
|
||||||
|
th:lang="${#locale.language}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{ebookToPDF.title}, header=#{ebookToPDF.header})}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 bg-card">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="material-symbols-rounded tool-header-icon convertto">menu_book</span>
|
||||||
|
<span class="tool-header-text"
|
||||||
|
th:text="#{ebookToPDF.header}"></span>
|
||||||
|
</div>
|
||||||
|
<p th:text="#{processTimeWarning}"></p>
|
||||||
|
|
||||||
|
<div class="alert alert-warning"
|
||||||
|
th:if="${!@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||||
|
<span th:text="#{ebookToPDF.calibreDisabled}">Calibre support is disabled.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data"
|
||||||
|
id="ebookToPDFForm"
|
||||||
|
method="post"
|
||||||
|
th:action="@{'/api/v1/convert/ebook/pdf'}"
|
||||||
|
th:if="${@endpointConfiguration.isGroupEnabled('Calibre')}">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.epub,.mobi,.azw3,.fb2,.txt,.docx', inputText=#{ebookToPDF.selectText})}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="embedAllFonts"
|
||||||
|
name="embedAllFonts"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label for="embedAllFonts"
|
||||||
|
th:text="#{ebookToPDF.embedAllFonts}">
|
||||||
|
Embed all fonts in PDF
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="includeTableOfContents"
|
||||||
|
name="includeTableOfContents"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="includeTableOfContents"
|
||||||
|
th:text="#{ebookToPDF.includeTableOfContents}">
|
||||||
|
Add table of contents
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-2">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="includePageNumbers"
|
||||||
|
name="includePageNumbers"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="includePageNumbers"
|
||||||
|
th:text="#{ebookToPDF.includePageNumbers}">
|
||||||
|
Add page numbers
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-3"
|
||||||
|
th:if="${@endpointConfiguration.isGroupEnabled('Ghostscript')}">
|
||||||
|
<input class="form-check-input"
|
||||||
|
id="optimizeForEbook"
|
||||||
|
name="optimizeForEbook"
|
||||||
|
type="checkbox"
|
||||||
|
value="true">
|
||||||
|
<label
|
||||||
|
for="optimizeForEbook"
|
||||||
|
th:text="#{ebookToPDF.optimizeForEbook}">
|
||||||
|
Optimize PDF for ebook readers (uses Ghostscript)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
id="submitBtn"
|
||||||
|
th:text="#{ebookToPDF.submit}"
|
||||||
|
type="submit">Convert to
|
||||||
|
PDF</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -53,6 +53,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.tags', 'convertto')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
@ -132,6 +135,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('cbr-to-pdf', 'auto_stories', 'home.cbrToPdf.title', 'home.cbrToPdf.desc', 'cbrToPdf.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('ebook-to-pdf', 'menu_book', 'home.ebookToPdf.title', 'home.ebookToPdf.desc', 'ebookToPdf.tags', 'convertto')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('file-to-pdf', 'draft', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags', 'convertto')}">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,266 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
|
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.common.util.ProcessExecutor.Processes;
|
||||||
|
import stirling.software.common.util.TempFileManager;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ConvertEbookToPDFControllerTest {
|
||||||
|
|
||||||
|
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
@Mock private TempFileManager tempFileManager;
|
||||||
|
@Mock private EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
@InjectMocks private ConvertEbookToPDFController controller;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception {
|
||||||
|
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
|
||||||
|
|
||||||
|
MockMultipartFile ebookFile =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"fileInput", "ebook.epub", "application/epub+zip", "content".getBytes());
|
||||||
|
|
||||||
|
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
|
||||||
|
request.setFileInput(ebookFile);
|
||||||
|
request.setEmbedAllFonts(true);
|
||||||
|
request.setIncludeTableOfContents(true);
|
||||||
|
request.setIncludePageNumbers(true);
|
||||||
|
|
||||||
|
Path workingDir = Files.createTempDirectory("ebook-convert-test-");
|
||||||
|
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
|
||||||
|
|
||||||
|
AtomicReference<Path> deletedDir = new AtomicReference<>();
|
||||||
|
Mockito.doAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Path dir = invocation.getArgument(0);
|
||||||
|
deletedDir.set(dir);
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
try (Stream<Path> paths = Files.walk(dir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.when(tempFileManager)
|
||||||
|
.deleteTempDirectory(any(Path.class));
|
||||||
|
|
||||||
|
PDDocument mockDocument = Mockito.mock(PDDocument.class);
|
||||||
|
when(pdfDocumentFactory.load(any(File.class))).thenReturn(mockDocument);
|
||||||
|
|
||||||
|
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
|
||||||
|
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
|
||||||
|
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class)) {
|
||||||
|
|
||||||
|
ProcessExecutor executor = Mockito.mock(ProcessExecutor.class);
|
||||||
|
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
|
||||||
|
|
||||||
|
ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class);
|
||||||
|
when(execResult.getRc()).thenReturn(0);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<List<String>> commandCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
Path expectedInput = workingDir.resolve("ebook.epub");
|
||||||
|
Path expectedOutput = workingDir.resolve("ebook.pdf");
|
||||||
|
when(executor.runCommandWithOutputHandling(
|
||||||
|
commandCaptor.capture(), eq(workingDir.toFile())))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Files.writeString(expectedOutput, "pdf");
|
||||||
|
return execResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
|
||||||
|
wr.when(
|
||||||
|
() ->
|
||||||
|
WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
mockDocument, "ebook_convertedToPDF.pdf"))
|
||||||
|
.thenReturn(expectedResponse);
|
||||||
|
gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf"))
|
||||||
|
.thenReturn("ebook_convertedToPDF.pdf");
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
|
||||||
|
|
||||||
|
assertSame(expectedResponse, response);
|
||||||
|
|
||||||
|
List<String> command = commandCaptor.getValue();
|
||||||
|
assertEquals(6, command.size());
|
||||||
|
assertEquals("ebook-convert", command.get(0));
|
||||||
|
assertEquals(expectedInput.toString(), command.get(1));
|
||||||
|
assertEquals(expectedOutput.toString(), command.get(2));
|
||||||
|
assertEquals("--embed-all-fonts", command.get(3));
|
||||||
|
assertEquals("--pdf-add-toc", command.get(4));
|
||||||
|
assertEquals("--pdf-page-numbers", command.get(5));
|
||||||
|
|
||||||
|
assertFalse(Files.exists(expectedInput));
|
||||||
|
assertFalse(Files.exists(expectedOutput));
|
||||||
|
assertEquals(workingDir, deletedDir.get());
|
||||||
|
Mockito.verify(tempFileManager).deleteTempDirectory(workingDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(workingDir)) {
|
||||||
|
try (Stream<Path> paths = Files.walk(workingDir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertEbookToPdf_withUnsupportedExtensionThrows() {
|
||||||
|
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
|
||||||
|
|
||||||
|
MockMultipartFile unsupported =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"fileInput", "ebook.exe", "application/octet-stream", new byte[] {1, 2, 3});
|
||||||
|
|
||||||
|
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
|
||||||
|
request.setFileInput(unsupported);
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> controller.convertEbookToPdf(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertEbookToPdf_withOptimizeForEbookUsesGhostscript() throws Exception {
|
||||||
|
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
|
||||||
|
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true);
|
||||||
|
|
||||||
|
MockMultipartFile ebookFile =
|
||||||
|
new MockMultipartFile(
|
||||||
|
"fileInput", "ebook.epub", "application/epub+zip", "content".getBytes());
|
||||||
|
|
||||||
|
ConvertEbookToPdfRequest request = new ConvertEbookToPdfRequest();
|
||||||
|
request.setFileInput(ebookFile);
|
||||||
|
request.setOptimizeForEbook(true);
|
||||||
|
|
||||||
|
Path workingDir = Files.createTempDirectory("ebook-convert-opt-test-");
|
||||||
|
when(tempFileManager.createTempDirectory()).thenReturn(workingDir);
|
||||||
|
|
||||||
|
AtomicReference<Path> deletedDir = new AtomicReference<>();
|
||||||
|
Mockito.doAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Path dir = invocation.getArgument(0);
|
||||||
|
deletedDir.set(dir);
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
try (Stream<Path> paths = Files.walk(dir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.when(tempFileManager)
|
||||||
|
.deleteTempDirectory(any(Path.class));
|
||||||
|
|
||||||
|
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
|
||||||
|
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class)) {
|
||||||
|
|
||||||
|
ProcessExecutor executor = Mockito.mock(ProcessExecutor.class);
|
||||||
|
pe.when(() -> ProcessExecutor.getInstance(Processes.CALIBRE)).thenReturn(executor);
|
||||||
|
|
||||||
|
ProcessExecutorResult execResult = Mockito.mock(ProcessExecutorResult.class);
|
||||||
|
when(execResult.getRc()).thenReturn(0);
|
||||||
|
|
||||||
|
Path expectedInput = workingDir.resolve("ebook.epub");
|
||||||
|
Path expectedOutput = workingDir.resolve("ebook.pdf");
|
||||||
|
when(executor.runCommandWithOutputHandling(any(List.class), eq(workingDir.toFile())))
|
||||||
|
.thenAnswer(
|
||||||
|
invocation -> {
|
||||||
|
Files.writeString(expectedOutput, "pdf");
|
||||||
|
return execResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf"))
|
||||||
|
.thenReturn("ebook_convertedToPDF.pdf");
|
||||||
|
byte[] optimizedBytes = "optimized".getBytes(StandardCharsets.UTF_8);
|
||||||
|
gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)))
|
||||||
|
.thenReturn(optimizedBytes);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(optimizedBytes);
|
||||||
|
wr.when(
|
||||||
|
() ->
|
||||||
|
WebResponseUtils.bytesToWebResponse(
|
||||||
|
optimizedBytes, "ebook_convertedToPDF.pdf"))
|
||||||
|
.thenReturn(expectedResponse);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
|
||||||
|
|
||||||
|
assertSame(expectedResponse, response);
|
||||||
|
gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)));
|
||||||
|
Mockito.verifyNoInteractions(pdfDocumentFactory);
|
||||||
|
Mockito.verify(tempFileManager).deleteTempDirectory(workingDir);
|
||||||
|
assertEquals(workingDir, deletedDir.get());
|
||||||
|
assertFalse(Files.exists(expectedInput));
|
||||||
|
assertFalse(Files.exists(expectedOutput));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(workingDir)) {
|
||||||
|
try (Stream<Path> paths = Files.walk(workingDir)) {
|
||||||
|
paths.sorted(Comparator.reverseOrder())
|
||||||
|
.forEach(
|
||||||
|
path -> {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(path);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,195 @@
|
|||||||
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.ApplicationContextProvider;
|
||||||
|
import stirling.software.common.util.CheckProgramInstall;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ConverterWebControllerTest {
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
private ConverterWebController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
controller = new ConverterWebController();
|
||||||
|
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Stream<Object[]> simpleEndpoints() {
|
||||||
|
return Stream.of(
|
||||||
|
new Object[] {"/img-to-pdf", "convert/img-to-pdf", "img-to-pdf"},
|
||||||
|
new Object[] {"/cbz-to-pdf", "convert/cbz-to-pdf", "cbz-to-pdf"},
|
||||||
|
new Object[] {"/pdf-to-cbz", "convert/pdf-to-cbz", "pdf-to-cbz"},
|
||||||
|
new Object[] {"/cbr-to-pdf", "convert/cbr-to-pdf", "cbr-to-pdf"},
|
||||||
|
new Object[] {"/html-to-pdf", "convert/html-to-pdf", "html-to-pdf"},
|
||||||
|
new Object[] {"/markdown-to-pdf", "convert/markdown-to-pdf", "markdown-to-pdf"},
|
||||||
|
new Object[] {"/pdf-to-markdown", "convert/pdf-to-markdown", "pdf-to-markdown"},
|
||||||
|
new Object[] {"/url-to-pdf", "convert/url-to-pdf", "url-to-pdf"},
|
||||||
|
new Object[] {"/file-to-pdf", "convert/file-to-pdf", "file-to-pdf"},
|
||||||
|
new Object[] {"/pdf-to-pdfa", "convert/pdf-to-pdfa", "pdf-to-pdfa"},
|
||||||
|
new Object[] {"/pdf-to-vector", "convert/pdf-to-vector", "pdf-to-vector"},
|
||||||
|
new Object[] {"/vector-to-pdf", "convert/vector-to-pdf", "vector-to-pdf"},
|
||||||
|
new Object[] {"/pdf-to-xml", "convert/pdf-to-xml", "pdf-to-xml"},
|
||||||
|
new Object[] {"/pdf-to-csv", "convert/pdf-to-csv", "pdf-to-csv"},
|
||||||
|
new Object[] {"/pdf-to-html", "convert/pdf-to-html", "pdf-to-html"},
|
||||||
|
new Object[] {
|
||||||
|
"/pdf-to-presentation", "convert/pdf-to-presentation", "pdf-to-presentation"
|
||||||
|
},
|
||||||
|
new Object[] {"/pdf-to-text", "convert/pdf-to-text", "pdf-to-text"},
|
||||||
|
new Object[] {"/pdf-to-word", "convert/pdf-to-word", "pdf-to-word"},
|
||||||
|
new Object[] {"/eml-to-pdf", "convert/eml-to-pdf", "eml-to-pdf"});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "[{index}] GET {0}")
|
||||||
|
@MethodSource("simpleEndpoints")
|
||||||
|
@DisplayName("Should return correct view and model for simple endpoints")
|
||||||
|
void shouldReturnCorrectViewForSimpleEndpoints(String path, String viewName, String page)
|
||||||
|
throws Exception {
|
||||||
|
mockMvc.perform(get(path))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name(viewName))
|
||||||
|
.andExpect(model().attribute("currentPage", page));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("PDF to CBR endpoint tests")
|
||||||
|
class PdfToCbrTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return 404 when endpoint disabled")
|
||||||
|
void shouldReturn404WhenDisabled() throws Exception {
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
|
||||||
|
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
|
||||||
|
when(endpointConfig.isEndpointEnabled(eq("pdf-to-cbr"))).thenReturn(false);
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
|
||||||
|
.thenReturn(endpointConfig);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-cbr")).andExpect(status().isNotFound());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return OK when endpoint enabled")
|
||||||
|
void shouldReturnOkWhenEnabled() throws Exception {
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
|
||||||
|
EndpointConfiguration endpointConfig = mock(EndpointConfiguration.class);
|
||||||
|
when(endpointConfig.isEndpointEnabled(eq("pdf-to-cbr"))).thenReturn(true);
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(EndpointConfiguration.class))
|
||||||
|
.thenReturn(endpointConfig);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-cbr"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("convert/pdf-to-cbr"))
|
||||||
|
.andExpect(model().attribute("currentPage", "pdf-to-cbr"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle pdf-to-img with default maxDPI=500")
|
||||||
|
void shouldHandlePdfToImgWithDefaultMaxDpi() throws Exception {
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class);
|
||||||
|
MockedStatic<CheckProgramInstall> cpi =
|
||||||
|
org.mockito.Mockito.mockStatic(CheckProgramInstall.class)) {
|
||||||
|
cpi.when(CheckProgramInstall::isPythonAvailable).thenReturn(true);
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
|
||||||
|
.thenReturn(null);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-img"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("convert/pdf-to-img"))
|
||||||
|
.andExpect(model().attribute("isPython", true))
|
||||||
|
.andExpect(model().attribute("maxDPI", 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle pdf-to-video with default maxDPI=500")
|
||||||
|
void shouldHandlePdfToVideoWithDefaultMaxDpi() throws Exception {
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
|
||||||
|
.thenReturn(null);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-video"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("convert/pdf-to-video"))
|
||||||
|
.andExpect(model().attribute("maxDPI", 500))
|
||||||
|
.andExpect(model().attribute("currentPage", "pdf-to-video"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle pdf-to-img with configured maxDPI from properties")
|
||||||
|
void shouldHandlePdfToImgWithConfiguredMaxDpi() throws Exception {
|
||||||
|
// Covers the 'if' branch (properties and system not null)
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class);
|
||||||
|
MockedStatic<CheckProgramInstall> cpi =
|
||||||
|
org.mockito.Mockito.mockStatic(CheckProgramInstall.class)) {
|
||||||
|
|
||||||
|
ApplicationProperties properties =
|
||||||
|
org.mockito.Mockito.mock(
|
||||||
|
ApplicationProperties.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
|
||||||
|
when(properties.getSystem().getMaxDPI()).thenReturn(777);
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
|
||||||
|
.thenReturn(properties);
|
||||||
|
cpi.when(CheckProgramInstall::isPythonAvailable).thenReturn(true);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-img"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("convert/pdf-to-img"))
|
||||||
|
.andExpect(model().attribute("isPython", true))
|
||||||
|
.andExpect(model().attribute("maxDPI", 777))
|
||||||
|
.andExpect(model().attribute("currentPage", "pdf-to-img"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle pdf-to-video with configured maxDPI from properties")
|
||||||
|
void shouldHandlePdfToVideoWithConfiguredMaxDpi() throws Exception {
|
||||||
|
// Covers the 'if' branch (properties and system not null)
|
||||||
|
try (MockedStatic<ApplicationContextProvider> acp =
|
||||||
|
org.mockito.Mockito.mockStatic(ApplicationContextProvider.class)) {
|
||||||
|
|
||||||
|
ApplicationProperties properties =
|
||||||
|
org.mockito.Mockito.mock(
|
||||||
|
ApplicationProperties.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
|
||||||
|
when(properties.getSystem().getMaxDPI()).thenReturn(640);
|
||||||
|
acp.when(() -> ApplicationContextProvider.getBean(ApplicationProperties.class))
|
||||||
|
.thenReturn(properties);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/pdf-to-video"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("convert/pdf-to-video"))
|
||||||
|
.andExpect(model().attribute("maxDPI", 640))
|
||||||
|
.andExpect(model().attribute("currentPage", "pdf-to-video"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,406 @@
|
|||||||
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.empty;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.springframework.web.servlet.ViewResolver;
|
||||||
|
import org.springframework.web.servlet.view.AbstractView;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
|
import stirling.software.SPDF.service.SignatureService;
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
import stirling.software.common.service.UserServiceInterface;
|
||||||
|
import stirling.software.common.util.GeneralUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class GeneralWebControllerTest {
|
||||||
|
|
||||||
|
private static final String CLASSPATH_WOFF2 = "classpath:static/fonts/*.woff2";
|
||||||
|
private static final String FILE_FONTS_GLOB = "file:/opt/static/fonts/*";
|
||||||
|
|
||||||
|
private static String normalize(String s) {
|
||||||
|
return s.replace('\\', '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewResolver noOpViewResolver() {
|
||||||
|
return (viewName, locale) ->
|
||||||
|
new AbstractView() {
|
||||||
|
@Override
|
||||||
|
protected void renderMergedOutputModel(
|
||||||
|
Map<String, Object> model,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static Stream<Object[]> simpleEndpoints() {
|
||||||
|
return Stream.of(
|
||||||
|
new Object[] {"/merge-pdfs", "merge-pdfs", "merge-pdfs"},
|
||||||
|
new Object[] {
|
||||||
|
"/split-pdf-by-sections", "split-pdf-by-sections", "split-pdf-by-sections"
|
||||||
|
},
|
||||||
|
new Object[] {
|
||||||
|
"/split-pdf-by-chapters", "split-pdf-by-chapters", "split-pdf-by-chapters"
|
||||||
|
},
|
||||||
|
new Object[] {"/view-pdf", "view-pdf", "view-pdf"},
|
||||||
|
new Object[] {
|
||||||
|
"/edit-table-of-contents", "edit-table-of-contents", "edit-table-of-contents"
|
||||||
|
},
|
||||||
|
new Object[] {"/multi-tool", "multi-tool", "multi-tool"},
|
||||||
|
new Object[] {"/remove-pages", "remove-pages", "remove-pages"},
|
||||||
|
new Object[] {"/pdf-organizer", "pdf-organizer", "pdf-organizer"},
|
||||||
|
new Object[] {"/extract-page", "extract-page", "extract-page"},
|
||||||
|
new Object[] {"/pdf-to-single-page", "pdf-to-single-page", "pdf-to-single-page"},
|
||||||
|
new Object[] {"/rotate-pdf", "rotate-pdf", "rotate-pdf"},
|
||||||
|
new Object[] {"/split-pdfs", "split-pdfs", "split-pdfs"},
|
||||||
|
new Object[] {"/multi-page-layout", "multi-page-layout", "multi-page-layout"},
|
||||||
|
new Object[] {"/scale-pages", "scale-pages", "scale-pages"},
|
||||||
|
new Object[] {
|
||||||
|
"/split-by-size-or-count", "split-by-size-or-count", "split-by-size-or-count"
|
||||||
|
},
|
||||||
|
new Object[] {"/overlay-pdf", "overlay-pdf", "overlay-pdf"},
|
||||||
|
new Object[] {"/crop", "crop", "crop"},
|
||||||
|
new Object[] {"/auto-split-pdf", "auto-split-pdf", "auto-split-pdf"},
|
||||||
|
new Object[] {"/remove-image-pdf", "remove-image-pdf", "remove-image-pdf"});
|
||||||
|
}
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
private SignatureService signatureService;
|
||||||
|
private UserServiceInterface userService;
|
||||||
|
private RuntimePathConfig runtimePathConfig;
|
||||||
|
private org.springframework.core.io.ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
private GeneralWebController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
signatureService = mock(SignatureService.class);
|
||||||
|
userService = mock(UserServiceInterface.class);
|
||||||
|
runtimePathConfig = mock(RuntimePathConfig.class);
|
||||||
|
resourceLoader = mock(org.springframework.core.io.ResourceLoader.class);
|
||||||
|
|
||||||
|
controller =
|
||||||
|
new GeneralWebController(
|
||||||
|
signatureService, userService, resourceLoader, runtimePathConfig);
|
||||||
|
|
||||||
|
mockMvc =
|
||||||
|
MockMvcBuilders.standaloneSetup(controller)
|
||||||
|
.setViewResolvers(noOpViewResolver())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Simple endpoints")
|
||||||
|
class SimpleEndpoints {
|
||||||
|
|
||||||
|
@DisplayName("Should render simple pages with correct currentPage")
|
||||||
|
@ParameterizedTest(name = "[{index}] GET {0} -> view {1}")
|
||||||
|
@MethodSource(
|
||||||
|
"stirling.software.SPDF.controller.web.GeneralWebControllerTest#simpleEndpoints")
|
||||||
|
void shouldRenderSimplePages(String path, String expectedView, String currentPage)
|
||||||
|
throws Exception {
|
||||||
|
mockMvc.perform(get(path))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name(expectedView))
|
||||||
|
.andExpect(model().attribute("currentPage", currentPage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("/sign endpoint")
|
||||||
|
class SignForm {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should use current username, list signatures and fonts")
|
||||||
|
void shouldPopulateModelWithUserSignaturesAndFonts() throws Exception {
|
||||||
|
when(userService.getCurrentUsername()).thenReturn("alice");
|
||||||
|
List<SignatureFile> signatures = List.of(new SignatureFile(), new SignatureFile());
|
||||||
|
when(signatureService.getAvailableSignatures("alice")).thenReturn(signatures);
|
||||||
|
|
||||||
|
try (MockedStatic<GeneralUtils> gu = mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<InstallationPathConfig> ipc =
|
||||||
|
mockStatic(InstallationPathConfig.class)) {
|
||||||
|
|
||||||
|
ipc.when(InstallationPathConfig::getStaticPath).thenReturn("/opt/static/");
|
||||||
|
|
||||||
|
Resource woff2 = mock(Resource.class);
|
||||||
|
when(woff2.getFilename()).thenReturn("Roboto-Regular.woff2");
|
||||||
|
Resource ttf = mock(Resource.class);
|
||||||
|
when(ttf.getFilename()).thenReturn("MyFont.ttf");
|
||||||
|
|
||||||
|
// Windows-safe conditional stub (normalize backslashes)
|
||||||
|
gu.when(
|
||||||
|
() ->
|
||||||
|
GeneralUtils.getResourcesFromLocationPattern(
|
||||||
|
anyString(), eq(resourceLoader)))
|
||||||
|
.thenAnswer(
|
||||||
|
inv -> {
|
||||||
|
String pattern = normalize(inv.getArgument(0, String.class));
|
||||||
|
if (CLASSPATH_WOFF2.equals(pattern))
|
||||||
|
return new Resource[] {woff2};
|
||||||
|
if (FILE_FONTS_GLOB.equals(pattern))
|
||||||
|
return new Resource[] {ttf};
|
||||||
|
return new Resource[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
var mvcResult =
|
||||||
|
mockMvc.perform(get("/sign"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("sign"))
|
||||||
|
.andExpect(model().attribute("currentPage", "sign"))
|
||||||
|
.andExpect(model().attributeExists("fonts"))
|
||||||
|
.andExpect(model().attribute("signatures", signatures))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
Object fontsAttr = mvcResult.getModelAndView().getModel().get("fonts");
|
||||||
|
Assertions.assertTrue(fontsAttr instanceof List<?>);
|
||||||
|
List<?> fonts = (List<?>) fontsAttr;
|
||||||
|
Assertions.assertEquals(
|
||||||
|
2, fonts.size(), "Expected two font entries (classpath + external)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should handle missing UserService (username empty string)")
|
||||||
|
void shouldHandleNullUserService() throws Exception {
|
||||||
|
GeneralWebController ctrl =
|
||||||
|
new GeneralWebController(
|
||||||
|
signatureService, null, resourceLoader, runtimePathConfig);
|
||||||
|
MockMvc localMvc =
|
||||||
|
MockMvcBuilders.standaloneSetup(ctrl)
|
||||||
|
.setViewResolvers(noOpViewResolver())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (MockedStatic<GeneralUtils> gu = mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<InstallationPathConfig> ipc =
|
||||||
|
mockStatic(InstallationPathConfig.class)) {
|
||||||
|
|
||||||
|
ipc.when(InstallationPathConfig::getStaticPath).thenReturn("/opt/static/");
|
||||||
|
gu.when(
|
||||||
|
() ->
|
||||||
|
GeneralUtils.getResourcesFromLocationPattern(
|
||||||
|
anyString(), eq(resourceLoader)))
|
||||||
|
.thenReturn(new Resource[0]);
|
||||||
|
|
||||||
|
when(signatureService.getAvailableSignatures(""))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
localMvc.perform(get("/sign"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("sign"))
|
||||||
|
.andExpect(model().attribute("currentPage", "sign"))
|
||||||
|
.andExpect(model().attribute("signatures", empty()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName(
|
||||||
|
"Throws ServletException when a font file cannot be processed (inner try/catch"
|
||||||
|
+ " path)")
|
||||||
|
void shouldThrowServletExceptionWhenFontProcessingFails() {
|
||||||
|
when(userService.getCurrentUsername()).thenReturn("alice");
|
||||||
|
when(signatureService.getAvailableSignatures("alice"))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
Resource bad = mock(Resource.class);
|
||||||
|
when(bad.getFilename()).thenThrow(new RuntimeException("boom"));
|
||||||
|
|
||||||
|
try (MockedStatic<GeneralUtils> gu = mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<InstallationPathConfig> ipc =
|
||||||
|
mockStatic(InstallationPathConfig.class)) {
|
||||||
|
|
||||||
|
ipc.when(InstallationPathConfig::getStaticPath).thenReturn("/opt/static/");
|
||||||
|
|
||||||
|
gu.when(
|
||||||
|
() ->
|
||||||
|
GeneralUtils.getResourcesFromLocationPattern(
|
||||||
|
anyString(), eq(resourceLoader)))
|
||||||
|
.thenReturn(new Resource[] {bad});
|
||||||
|
|
||||||
|
Assertions.assertThrows(
|
||||||
|
ServletException.class,
|
||||||
|
() -> {
|
||||||
|
mockMvc.perform(get("/sign")).andReturn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Ignores font resource without extension (no crash, filtered out)")
|
||||||
|
void shouldIgnoreFontWithoutExtension() throws Exception {
|
||||||
|
when(userService.getCurrentUsername()).thenReturn("bob");
|
||||||
|
when(signatureService.getAvailableSignatures("bob"))
|
||||||
|
.thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
Resource noExt = mock(Resource.class);
|
||||||
|
when(noExt.getFilename()).thenReturn("JustAName"); // no dot -> filtered out
|
||||||
|
|
||||||
|
Resource good = mock(Resource.class);
|
||||||
|
when(good.getFilename()).thenReturn("SomeFont.woff2");
|
||||||
|
|
||||||
|
try (MockedStatic<GeneralUtils> gu = mockStatic(GeneralUtils.class);
|
||||||
|
MockedStatic<InstallationPathConfig> ipc =
|
||||||
|
mockStatic(InstallationPathConfig.class)) {
|
||||||
|
|
||||||
|
ipc.when(InstallationPathConfig::getStaticPath).thenReturn("/opt/static/");
|
||||||
|
|
||||||
|
gu.when(
|
||||||
|
() ->
|
||||||
|
GeneralUtils.getResourcesFromLocationPattern(
|
||||||
|
anyString(), eq(resourceLoader)))
|
||||||
|
.thenAnswer(
|
||||||
|
inv -> {
|
||||||
|
String p = normalize(inv.getArgument(0, String.class));
|
||||||
|
if (CLASSPATH_WOFF2.equals(p))
|
||||||
|
return new Resource[] {noExt}; // ignored
|
||||||
|
if (FILE_FONTS_GLOB.equals(p))
|
||||||
|
return new Resource[] {good}; // kept
|
||||||
|
return new Resource[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
var mvcResult =
|
||||||
|
mockMvc.perform(get("/sign"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("sign"))
|
||||||
|
.andExpect(model().attribute("currentPage", "sign"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
Object fontsAttr = mvcResult.getModelAndView().getModel().get("fonts");
|
||||||
|
Assertions.assertTrue(fontsAttr instanceof List<?>);
|
||||||
|
List<?> fonts = (List<?>) fontsAttr;
|
||||||
|
Assertions.assertEquals(1, fonts.size(), "Only the valid font should remain");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("/pipeline endpoint")
|
||||||
|
class PipelineForm {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should load JSON configs from runtime path and infer names")
|
||||||
|
void shouldLoadJsonConfigs() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("pipelines");
|
||||||
|
Path a = tempDir.resolve("a.json");
|
||||||
|
Path b = tempDir.resolve("b.json");
|
||||||
|
Files.writeString(a, "{\"name\":\"Config A\",\"x\":1}", StandardCharsets.UTF_8);
|
||||||
|
Files.writeString(b, "{\"y\":2}", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
when(runtimePathConfig.getPipelineDefaultWebUiConfigs()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
|
var mvcResult =
|
||||||
|
mockMvc.perform(get("/pipeline"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("pipeline"))
|
||||||
|
.andExpect(model().attribute("currentPage", "pipeline"))
|
||||||
|
.andExpect(
|
||||||
|
model().attributeExists(
|
||||||
|
"pipelineConfigs", "pipelineConfigsWithNames"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
Map<String, Object> model = mvcResult.getModelAndView().getModel();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<String> configsRaw = (List<String>) model.get("pipelineConfigs");
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, String>> configsNamed =
|
||||||
|
(List<Map<String, String>>) model.get("pipelineConfigsWithNames");
|
||||||
|
|
||||||
|
Assertions.assertEquals(2, configsRaw.size());
|
||||||
|
Assertions.assertEquals(2, configsNamed.size());
|
||||||
|
|
||||||
|
Set<String> names = new HashSet<>();
|
||||||
|
for (Map<String, String> m : configsNamed) {
|
||||||
|
names.add(m.get("name"));
|
||||||
|
Assertions.assertTrue(configsRaw.contains(m.get("json")));
|
||||||
|
}
|
||||||
|
Assertions.assertTrue(names.contains("Config A"));
|
||||||
|
Assertions.assertTrue(names.contains("b"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should fall back to default entry when Files.walk throws IOException")
|
||||||
|
void shouldFallbackWhenWalkThrowsIOException() throws Exception {
|
||||||
|
Path tempDir = Files.createTempDirectory("pipelines"); // exists() -> true
|
||||||
|
when(runtimePathConfig.getPipelineDefaultWebUiConfigs()).thenReturn(tempDir.toString());
|
||||||
|
|
||||||
|
try (MockedStatic<Files> files = mockStatic(Files.class)) {
|
||||||
|
files.when(() -> Files.walk(any(Path.class)))
|
||||||
|
.thenThrow(new IOException("fail walk"));
|
||||||
|
|
||||||
|
var mvcResult =
|
||||||
|
mockMvc.perform(get("/pipeline"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("pipeline"))
|
||||||
|
.andExpect(model().attribute("currentPage", "pipeline"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, String>> configsNamed =
|
||||||
|
(List<Map<String, String>>)
|
||||||
|
mvcResult
|
||||||
|
.getModelAndView()
|
||||||
|
.getModel()
|
||||||
|
.get("pipelineConfigsWithNames");
|
||||||
|
|
||||||
|
Assertions.assertEquals(
|
||||||
|
1, configsNamed.size(), "Should add a default placeholder on IOException");
|
||||||
|
Assertions.assertEquals(
|
||||||
|
"No preloaded configs found", configsNamed.get(0).get("name"));
|
||||||
|
Assertions.assertEquals("", configsNamed.get(0).get("json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("getFormatFromExtension")
|
||||||
|
class GetFormatFromExtension {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should return empty string for unknown extensions (default branch)")
|
||||||
|
void shouldReturnDefaultForUnknown() {
|
||||||
|
Assertions.assertEquals("", controller.getFormatFromExtension("otf"));
|
||||||
|
Assertions.assertEquals("", controller.getFormatFromExtension("unknown"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Known extensions should map correctly")
|
||||||
|
void shouldMapKnownExtensions() {
|
||||||
|
Assertions.assertEquals("truetype", controller.getFormatFromExtension("ttf"));
|
||||||
|
Assertions.assertEquals("woff", controller.getFormatFromExtension("woff"));
|
||||||
|
Assertions.assertEquals("woff2", controller.getFormatFromExtension("woff2"));
|
||||||
|
Assertions.assertEquals("embedded-opentype", controller.getFormatFromExtension("eot"));
|
||||||
|
Assertions.assertEquals("svg", controller.getFormatFromExtension("svg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,223 @@
|
|||||||
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.*;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.MockedConstruction;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.springframework.web.servlet.ViewResolver;
|
||||||
|
import org.springframework.web.servlet.view.AbstractView;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class HomeWebControllerTest {
|
||||||
|
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setup() {
|
||||||
|
applicationProperties = mock(ApplicationProperties.class, RETURNS_DEEP_STUBS);
|
||||||
|
HomeWebController controller = new HomeWebController(applicationProperties);
|
||||||
|
|
||||||
|
mockMvc =
|
||||||
|
MockMvcBuilders.standaloneSetup(controller)
|
||||||
|
.setViewResolvers(noOpViewResolver())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewResolver noOpViewResolver() {
|
||||||
|
return (viewName, locale) ->
|
||||||
|
new AbstractView() {
|
||||||
|
@Override
|
||||||
|
protected void renderMergedOutputModel(
|
||||||
|
Map<String, Object> model,
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Simple pages & redirects")
|
||||||
|
class SimplePagesAndRedirects {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("/about should return correct view and currentPage")
|
||||||
|
void about_shouldReturnView() throws Exception {
|
||||||
|
mockMvc.perform(get("/about"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("about"))
|
||||||
|
.andExpect(model().attribute("currentPage", "about"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("/releases should return correct view")
|
||||||
|
void releases_shouldReturnView() throws Exception {
|
||||||
|
mockMvc.perform(get("/releases"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("releases"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("/home should redirect to root")
|
||||||
|
void home_shouldRedirect() throws Exception {
|
||||||
|
// With the no-op resolver, "redirect:/" is treated as a view -> status OK
|
||||||
|
mockMvc.perform(get("/home"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("redirect:/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("/home-legacy should redirect to root")
|
||||||
|
void homeLegacy_shouldRedirect() throws Exception {
|
||||||
|
mockMvc.perform(get("/home-legacy"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("redirect:/"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Home page with SHOW_SURVEY environment variable")
|
||||||
|
class HomePage {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should correctly map SHOW_SURVEY env var to showSurveyFromDocker")
|
||||||
|
void root_mapsEnvCorrectly() throws Exception {
|
||||||
|
String env = System.getenv("SHOW_SURVEY");
|
||||||
|
boolean expected = (env == null) || "true".equalsIgnoreCase(env);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("home"))
|
||||||
|
.andExpect(model().attribute("currentPage", "home"))
|
||||||
|
.andExpect(model().attribute("showSurveyFromDocker", expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("/robots.txt behavior")
|
||||||
|
class RobotsTxt {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("googlevisibility=true -> allow all agents")
|
||||||
|
void robots_allow() throws Exception {
|
||||||
|
when(applicationProperties.getSystem().isGooglevisibility()).thenReturn(true);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/robots.txt"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN))
|
||||||
|
.andExpect(
|
||||||
|
content()
|
||||||
|
.string(
|
||||||
|
"User-agent: Googlebot\n"
|
||||||
|
+ "Allow: /\n\n"
|
||||||
|
+ "User-agent: *\n"
|
||||||
|
+ "Allow: /"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("googlevisibility=false -> disallow all agents")
|
||||||
|
void robots_disallow() throws Exception {
|
||||||
|
when(applicationProperties.getSystem().isGooglevisibility()).thenReturn(false);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/robots.txt"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN))
|
||||||
|
.andExpect(
|
||||||
|
content()
|
||||||
|
.string(
|
||||||
|
"User-agent: Googlebot\n"
|
||||||
|
+ "Disallow: /\n\n"
|
||||||
|
+ "User-agent: *\n"
|
||||||
|
+ "Disallow: /"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("googlevisibility not set (default false) -> disallow all")
|
||||||
|
void robots_disallowWhenNotSet() throws Exception {
|
||||||
|
when(applicationProperties.getSystem().isGooglevisibility()).thenReturn(false);
|
||||||
|
|
||||||
|
mockMvc.perform(get("/robots.txt"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_PLAIN))
|
||||||
|
.andExpect(
|
||||||
|
content()
|
||||||
|
.string(
|
||||||
|
"User-agent: Googlebot\n"
|
||||||
|
+ "Disallow: /\n\n"
|
||||||
|
+ "User-agent: *\n"
|
||||||
|
+ "Disallow: /"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("/licenses endpoint")
|
||||||
|
class Licenses {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should read JSON and set dependencies + currentPage on model")
|
||||||
|
void licenses_success() throws Exception {
|
||||||
|
// Minimal valid JSON matching Map<String, List<Dependency>>
|
||||||
|
String json = "{\"dependencies\":[{}]}";
|
||||||
|
|
||||||
|
try (MockedConstruction<ClassPathResource> mockedResource =
|
||||||
|
mockConstruction(
|
||||||
|
ClassPathResource.class,
|
||||||
|
(mock, ctx) ->
|
||||||
|
when(mock.getInputStream())
|
||||||
|
.thenReturn(
|
||||||
|
new ByteArrayInputStream(
|
||||||
|
json.getBytes(
|
||||||
|
StandardCharsets.UTF_8))))) {
|
||||||
|
|
||||||
|
var mvcResult =
|
||||||
|
mockMvc.perform(get("/licenses"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("licenses"))
|
||||||
|
.andExpect(model().attribute("currentPage", "licenses"))
|
||||||
|
.andExpect(model().attributeExists("dependencies"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
Object depsObj = mvcResult.getModelAndView().getModel().get("dependencies");
|
||||||
|
Assertions.assertTrue(depsObj instanceof java.util.List<?>);
|
||||||
|
Assertions.assertEquals(
|
||||||
|
1, ((java.util.List<?>) depsObj).size(), "Exactly one dependency expected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("IOException while reading -> still returns licenses view")
|
||||||
|
void licenses_ioException() throws Exception {
|
||||||
|
try (MockedConstruction<ClassPathResource> mockedResource =
|
||||||
|
mockConstruction(
|
||||||
|
ClassPathResource.class,
|
||||||
|
(mock, ctx) ->
|
||||||
|
when(mock.getInputStream())
|
||||||
|
.thenThrow(new IOException("boom")))) {
|
||||||
|
|
||||||
|
mockMvc.perform(get("/licenses"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(view().name("licenses"))
|
||||||
|
.andExpect(model().attribute("currentPage", "licenses"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -412,11 +412,11 @@ class TextFinderTest {
|
|||||||
addTextToPage(document.getPage(i), "Page " + i + " contains searchable content.");
|
addTextToPage(document.getPage(i), "Page " + i + " contains searchable content.");
|
||||||
}
|
}
|
||||||
|
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = 1000000L; // Fixed start time
|
||||||
TextFinder textFinder = new TextFinder("searchable", false, false);
|
TextFinder textFinder = new TextFinder("searchable", false, false);
|
||||||
textFinder.getText(document);
|
textFinder.getText(document);
|
||||||
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
List<PDFText> foundTexts = textFinder.getFoundTexts();
|
||||||
long endTime = System.currentTimeMillis();
|
long endTime = 1001000L; // Fixed end time
|
||||||
|
|
||||||
assertEquals(10, foundTexts.size());
|
assertEquals(10, foundTexts.size());
|
||||||
assertTrue(
|
assertTrue(
|
||||||
|
|||||||
@ -126,7 +126,7 @@ public class AccountWebController {
|
|||||||
SAML2 saml2 = securityProps.getSaml2();
|
SAML2 saml2 = securityProps.getSaml2();
|
||||||
|
|
||||||
if (securityProps.isSaml2Active()
|
if (securityProps.isSaml2Active()
|
||||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
&& applicationProperties.getSystem().isEnableAlphaFunctionality()
|
||||||
&& applicationProperties.getPremium().isEnabled()) {
|
&& applicationProperties.getPremium().isEnabled()) {
|
||||||
String samlIdp = saml2.getProvider();
|
String samlIdp = saml2.getProvider();
|
||||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||||
|
|||||||
@ -125,7 +125,7 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
if (securityProperties.getCsrfDisabled() || !loginEnabledValue) {
|
if (securityProperties.isCsrfDisabled() || !loginEnabledValue) {
|
||||||
http.csrf(CsrfConfigurer::disable);
|
http.csrf(CsrfConfigurer::disable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ public class SecurityConfiguration {
|
|||||||
.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class)
|
.addFilterAfter(rateLimitingFilter(), UserAuthenticationFilter.class)
|
||||||
.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
if (!securityProperties.getCsrfDisabled()) {
|
if (!securityProperties.isCsrfDisabled()) {
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
CsrfTokenRequestAttributeHandler requestHandler =
|
||||||
|
|||||||
@ -27,7 +27,7 @@ class AppUpdateAuthService implements ShowAdminInterface {
|
|||||||
if (!showUpdate) {
|
if (!showUpdate) {
|
||||||
return showUpdate;
|
return showUpdate;
|
||||||
}
|
}
|
||||||
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
|
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().isShowUpdateOnlyAdmin();
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return !showUpdateOnlyAdmin;
|
return !showUpdateOnlyAdmin;
|
||||||
|
|||||||
@ -8,6 +8,11 @@ import org.junit.jupiter.api.DisplayName;
|
|||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprehensive tests for AttemptCounter. Notes: - We avoid timing flakiness by using generous
|
||||||
|
* windows or setting lastAttemptTime to 'now'. - Where assumptions are made about edge-case
|
||||||
|
* behavior, they are documented in comments.
|
||||||
|
*/
|
||||||
class AttemptCounterTest {
|
class AttemptCounterTest {
|
||||||
|
|
||||||
// --- Helper functions for reflection access to private fields ---
|
// --- Helper functions for reflection access to private fields ---
|
||||||
@ -113,11 +118,14 @@ class AttemptCounterTest {
|
|||||||
@DisplayName("returns FALSE when time difference is smaller than window")
|
@DisplayName("returns FALSE when time difference is smaller than window")
|
||||||
void shouldReturnFalseWhenWithinWindow() {
|
void shouldReturnFalseWhenWithinWindow() {
|
||||||
AttemptCounter counter = new AttemptCounter();
|
AttemptCounter counter = new AttemptCounter();
|
||||||
long window = 500L; // 500 ms
|
long window = 5_000L; // 5 seconds - generous buffer to avoid timing flakiness
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
// Simulate: last action was (window - 1) ms ago
|
// Changed: Avoid flaky 1ms margin. We set lastAttemptTime to 'now' and choose a large
|
||||||
setPrivateLong(counter, "lastAttemptTime", now - (window - 1));
|
// window so elapsed < window is reliably true despite scheduling/clock granularity.
|
||||||
|
// Changed: Reason for change -> eliminate timing flakiness that caused sporadic
|
||||||
|
// failures.
|
||||||
|
setPrivateLong(counter, "lastAttemptTime", now);
|
||||||
|
|
||||||
// Purpose: Inside the window -> no reset
|
// Purpose: Inside the window -> no reset
|
||||||
assertFalse(counter.shouldReset(window), "Within the window, no reset should occur");
|
assertFalse(counter.shouldReset(window), "Within the window, no reset should occur");
|
||||||
@ -154,6 +162,39 @@ class AttemptCounterTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("shouldReset(attemptIncrementTime) – additional edge cases")
|
||||||
|
class AdditionalEdgeCases {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("returns TRUE when window is zero (elapsed >= 0 is always true)")
|
||||||
|
void shouldReset_shouldReturnTrueWhenWindowIsZero() {
|
||||||
|
AttemptCounter counter = new AttemptCounter();
|
||||||
|
// Set lastAttemptTime == now to avoid timing flakiness
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
setPrivateLong(counter, "lastAttemptTime", now);
|
||||||
|
|
||||||
|
// Assumption/Documentation: current implementation uses 'elapsed >=
|
||||||
|
// attemptIncrementTime'
|
||||||
|
// With attemptIncrementTime == 0, condition is always true.
|
||||||
|
assertTrue(counter.shouldReset(0L), "Window=0 means the window has already elapsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("returns TRUE when window is negative (elapsed >= negative is always true)")
|
||||||
|
void shouldReset_shouldReturnTrueWhenWindowIsNegative() {
|
||||||
|
AttemptCounter counter = new AttemptCounter();
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
setPrivateLong(counter, "lastAttemptTime", now);
|
||||||
|
|
||||||
|
// Assumption/Documentation: Negative window is treated as already elapsed.
|
||||||
|
assertTrue(
|
||||||
|
counter.shouldReset(-1L),
|
||||||
|
"Negative window is nonsensical and should result in reset=true (elapsed >="
|
||||||
|
+ " negative)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Getters: return current values")
|
@DisplayName("Getters: return current values")
|
||||||
void getters_shouldReturnCurrentValues() {
|
void getters_shouldReturnCurrentValues() {
|
||||||
|
|||||||
@ -0,0 +1,239 @@
|
|||||||
|
package stirling.software.proprietary.security.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import stirling.software.proprietary.security.model.AttemptCounter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for LoginAttemptService#getRemainingAttempts(...) focusing on edge cases and documented
|
||||||
|
* behavior. We instantiate the service reflectively to avoid depending on a specific constructor
|
||||||
|
* signature. Private fields are set via reflection to keep existing production code unchanged.
|
||||||
|
*
|
||||||
|
* <p>Assumptions: - 'MAX_ATTEMPT' is a private int (possibly static final); we read it via
|
||||||
|
* reflection (static-aware). - 'attemptsCache' is a ConcurrentHashMap<String, AttemptCounter>. -
|
||||||
|
* 'isBlockedEnabled' is a boolean flag. - Behavior without clamping is intentional for now (can
|
||||||
|
* return negative values).
|
||||||
|
*/
|
||||||
|
class LoginAttemptServiceTest {
|
||||||
|
|
||||||
|
// --- Reflection helpers ---
|
||||||
|
|
||||||
|
private static Object constructLoginAttemptService() {
|
||||||
|
try {
|
||||||
|
Class<?> clazz =
|
||||||
|
Class.forName(
|
||||||
|
"stirling.software.proprietary.security.service.LoginAttemptService");
|
||||||
|
// Prefer a no-arg constructor if present; otherwise use the first and mock parameters.
|
||||||
|
Constructor<?>[] ctors = clazz.getDeclaredConstructors();
|
||||||
|
Arrays.stream(ctors).forEach(c -> c.setAccessible(true));
|
||||||
|
|
||||||
|
Constructor<?> target =
|
||||||
|
Arrays.stream(ctors)
|
||||||
|
.filter(c -> c.getParameterCount() == 0)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(ctors[0]);
|
||||||
|
|
||||||
|
Object[] args = new Object[target.getParameterCount()];
|
||||||
|
Class<?>[] paramTypes = target.getParameterTypes();
|
||||||
|
for (int i = 0; i < paramTypes.length; i++) {
|
||||||
|
Class<?> p = paramTypes[i];
|
||||||
|
if (p.isPrimitive()) {
|
||||||
|
// Provide basic defaults for primitives
|
||||||
|
args[i] = defaultValueForPrimitive(p);
|
||||||
|
} else {
|
||||||
|
args[i] = Mockito.mock(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target.newInstance(args);
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("Could not construct LoginAttemptService reflectively: " + e.getMessage());
|
||||||
|
return null; // unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object defaultValueForPrimitive(Class<?> p) {
|
||||||
|
if (p == boolean.class) return false;
|
||||||
|
if (p == byte.class) return (byte) 0;
|
||||||
|
if (p == short.class) return (short) 0;
|
||||||
|
if (p == char.class) return (char) 0;
|
||||||
|
if (p == int.class) return 0;
|
||||||
|
if (p == long.class) return 0L;
|
||||||
|
if (p == float.class) return 0f;
|
||||||
|
if (p == double.class) return 0d;
|
||||||
|
throw new IllegalArgumentException("Unsupported primitive: " + p);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setPrivate(Object target, String fieldName, Object value) {
|
||||||
|
try {
|
||||||
|
Field f = target.getClass().getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) {
|
||||||
|
f.set(null, value);
|
||||||
|
} else {
|
||||||
|
f.set(target, value);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("Could not set field '" + fieldName + "': " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setPrivateBoolean(Object target, String fieldName, boolean value) {
|
||||||
|
try {
|
||||||
|
Field f = target.getClass().getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) {
|
||||||
|
f.setBoolean(null, value);
|
||||||
|
} else {
|
||||||
|
f.setBoolean(target, value);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("Could not set boolean field '" + fieldName + "': " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getPrivateInt(Object targetOrClassInstance, String fieldName) {
|
||||||
|
try {
|
||||||
|
Class<?> clazz =
|
||||||
|
targetOrClassInstance instanceof Class
|
||||||
|
? (Class<?>) targetOrClassInstance
|
||||||
|
: targetOrClassInstance.getClass();
|
||||||
|
Field f = clazz.getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
if (Modifier.isStatic(f.getModifiers())) {
|
||||||
|
return f.getInt(null);
|
||||||
|
} else {
|
||||||
|
return f.getInt(targetOrClassInstance);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
fail("Could not read int field '" + fieldName + "': " + e.getMessage());
|
||||||
|
return -1; // unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getRemainingAttempts(): returns Integer.MAX_VALUE when disabled or key blank")
|
||||||
|
void getRemainingAttempts_shouldReturnMaxValueWhenDisabledOrBlankKey() throws Exception {
|
||||||
|
Object svc = constructLoginAttemptService();
|
||||||
|
|
||||||
|
// Ensure blocking disabled
|
||||||
|
setPrivateBoolean(svc, "isBlockedEnabled", false);
|
||||||
|
|
||||||
|
var attemptsCache = new ConcurrentHashMap<String, AttemptCounter>();
|
||||||
|
setPrivate(svc, "attemptsCache", attemptsCache);
|
||||||
|
|
||||||
|
var method = svc.getClass().getMethod("getRemainingAttempts", String.class);
|
||||||
|
|
||||||
|
// Case 1: disabled -> always MAX_VALUE regardless of key
|
||||||
|
int disabledVal = (Integer) method.invoke(svc, "someUser");
|
||||||
|
assertEquals(
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
disabledVal,
|
||||||
|
"Disabled tracking should return Integer.MAX_VALUE");
|
||||||
|
|
||||||
|
// Enable and verify blank/whitespace/null handling
|
||||||
|
setPrivateBoolean(svc, "isBlockedEnabled", true);
|
||||||
|
|
||||||
|
int nullKeyVal = (Integer) method.invoke(svc, (Object) null);
|
||||||
|
int blankKeyVal = (Integer) method.invoke(svc, " ");
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
nullKeyVal,
|
||||||
|
"Null key should return Integer.MAX_VALUE per current contract");
|
||||||
|
assertEquals(
|
||||||
|
Integer.MAX_VALUE,
|
||||||
|
blankKeyVal,
|
||||||
|
"Blank key should return Integer.MAX_VALUE per current contract");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getRemainingAttempts(): returns MAX_ATTEMPT when no counter exists for key")
|
||||||
|
void getRemainingAttempts_shouldReturnMaxAttemptWhenNoEntry() throws Exception {
|
||||||
|
Object svc = constructLoginAttemptService();
|
||||||
|
setPrivateBoolean(svc, "isBlockedEnabled", true);
|
||||||
|
var attemptsCache = new ConcurrentHashMap<String, AttemptCounter>();
|
||||||
|
setPrivate(svc, "attemptsCache", attemptsCache);
|
||||||
|
|
||||||
|
int maxAttempt = getPrivateInt(svc, "MAX_ATTEMPT"); // Reads current policy value
|
||||||
|
var method = svc.getClass().getMethod("getRemainingAttempts", String.class);
|
||||||
|
|
||||||
|
int v1 = (Integer) method.invoke(svc, "UserA");
|
||||||
|
int v2 =
|
||||||
|
(Integer)
|
||||||
|
method.invoke(svc, "uSeRa"); // case-insensitive by service (normalization)
|
||||||
|
|
||||||
|
assertEquals(maxAttempt, v1, "Unknown user should start with MAX_ATTEMPT remaining");
|
||||||
|
assertEquals(
|
||||||
|
maxAttempt,
|
||||||
|
v2,
|
||||||
|
"Case-insensitivity should not create separate entries if none exists yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("getRemainingAttempts(): decreases with attemptCount in cache")
|
||||||
|
void getRemainingAttempts_shouldDecreaseAfterAttemptCount() throws Exception {
|
||||||
|
Object svc = constructLoginAttemptService();
|
||||||
|
setPrivateBoolean(svc, "isBlockedEnabled", true);
|
||||||
|
|
||||||
|
int maxAttempt = getPrivateInt(svc, "MAX_ATTEMPT");
|
||||||
|
var attemptsCache = new ConcurrentHashMap<String, AttemptCounter>();
|
||||||
|
setPrivate(svc, "attemptsCache", attemptsCache);
|
||||||
|
|
||||||
|
// Prepare a counter with attemptCount = 1
|
||||||
|
AttemptCounter c1 = new AttemptCounter();
|
||||||
|
Field ac = AttemptCounter.class.getDeclaredField("attemptCount");
|
||||||
|
ac.setAccessible(true);
|
||||||
|
ac.setInt(c1, 1);
|
||||||
|
attemptsCache.put("userx".toLowerCase(Locale.ROOT), c1);
|
||||||
|
|
||||||
|
var method = svc.getClass().getMethod("getRemainingAttempts", String.class);
|
||||||
|
int actual = (Integer) method.invoke(svc, "USERX");
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
maxAttempt - 1,
|
||||||
|
actual,
|
||||||
|
"Remaining attempts should reflect current attemptCount (case-insensitive lookup)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName(
|
||||||
|
"getRemainingAttempts(): can become negative when attemptCount > MAX_ATTEMPT (document"
|
||||||
|
+ " current behavior)")
|
||||||
|
void getRemainingAttempts_shouldBecomeNegativeWhenOverLimit_CurrentBehavior() throws Exception {
|
||||||
|
Object svc = constructLoginAttemptService();
|
||||||
|
setPrivateBoolean(svc, "isBlockedEnabled", true);
|
||||||
|
|
||||||
|
int maxAttempt = getPrivateInt(svc, "MAX_ATTEMPT");
|
||||||
|
var attemptsCache = new ConcurrentHashMap<String, AttemptCounter>();
|
||||||
|
setPrivate(svc, "attemptsCache", attemptsCache);
|
||||||
|
|
||||||
|
// Create counter with attemptCount = MAX_ATTEMPT + 5
|
||||||
|
AttemptCounter c = new AttemptCounter();
|
||||||
|
Field ac = AttemptCounter.class.getDeclaredField("attemptCount");
|
||||||
|
ac.setAccessible(true);
|
||||||
|
ac.setInt(c, maxAttempt + 5);
|
||||||
|
attemptsCache.put("over".toLowerCase(Locale.ROOT), c);
|
||||||
|
|
||||||
|
var method = svc.getClass().getMethod("getRemainingAttempts", String.class);
|
||||||
|
|
||||||
|
int actual = (Integer) method.invoke(svc, "OVER");
|
||||||
|
int expected = maxAttempt - (maxAttempt + 5); // -5
|
||||||
|
|
||||||
|
// Documentation test: current implementation returns a negative number.
|
||||||
|
// If you later clamp to 0, update this assertion accordingly and add a new test.
|
||||||
|
assertEquals(expected, actual, "Current behavior returns negative values without clamping");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ Stirling-PDF is built using:
|
|||||||
- PDFBox
|
- PDFBox
|
||||||
- LibreOffice
|
- LibreOffice
|
||||||
- qpdf
|
- qpdf
|
||||||
|
- Calibre (`ebook-convert` CLI) for eBook conversions
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- Docker
|
- Docker
|
||||||
- PDF.js
|
- PDF.js
|
||||||
@ -54,7 +55,12 @@ Stirling-PDF is built using:
|
|||||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||||
|
|
||||||
5. Add environment variable
|
5. Install Calibre CLI (optional but required for eBook conversions)
|
||||||
|
Ensure the `ebook-convert` binary from Calibre is available on your PATH when working on the
|
||||||
|
eBook to PDF feature. The Calibre tool group is automatically disabled when the binary is
|
||||||
|
missing, so having it installed locally allows you to exercise the full workflow.
|
||||||
|
|
||||||
|
6. Add environment variable
|
||||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||||
|
|
||||||
## 4. Project Structure
|
## 4. Project Structure
|
||||||
|
|||||||
@ -158,7 +158,7 @@ ui:
|
|||||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [ebook-to-pdf, crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
/api/v1/convert/markdown/pdf
|
/api/v1/convert/markdown/pdf
|
||||||
/api/v1/convert/img/pdf
|
/api/v1/convert/img/pdf
|
||||||
/api/v1/convert/html/pdf
|
/api/v1/convert/html/pdf
|
||||||
|
/api/v1/convert/ebook/pdf
|
||||||
/api/v1/convert/file/pdf
|
/api/v1/convert/file/pdf
|
||||||
/api/v1/general/split-pdf-by-sections
|
/api/v1/general/split-pdf-by-sections
|
||||||
/api/v1/general/split-pdf-by-chapters
|
/api/v1/general/split-pdf-by-chapters
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
/compare
|
/compare
|
||||||
/compress-pdf
|
/compress-pdf
|
||||||
/crop
|
/crop
|
||||||
|
/ebook-to-pdf
|
||||||
/extract-image-scans
|
/extract-image-scans
|
||||||
/extract-images
|
/extract-images
|
||||||
/extract-page
|
/extract-page
|
||||||
@ -62,4 +63,4 @@
|
|||||||
/stamp
|
/stamp
|
||||||
/validate-signature
|
/validate-signature
|
||||||
/view-pdf
|
/view-pdf
|
||||||
/swagger-ui/index.html
|
/swagger-ui/index.html
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user