mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-22 04:09:22 +00:00
Merge pull request #4162 from Stirling-Tools/mainClone
V2 main merge upbranch
This commit is contained in:
commit
20245c67e0
@ -49,7 +49,7 @@
|
|||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
"java.format.enabled": true,
|
"java.format.enabled": true,
|
||||||
"java.format.settings.profile": "GoogleStyle",
|
"java.format.settings.profile": "GoogleStyle",
|
||||||
"java.format.settings.google.version": "1.26.0",
|
"java.format.settings.google.version": "1.28.0",
|
||||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||||
"java.saveActions.cleanup": true,
|
"java.saveActions.cleanup": true,
|
||||||
"java.cleanup.actions": [
|
"java.cleanup.actions": [
|
||||||
@ -79,9 +79,17 @@
|
|||||||
".venv*/",
|
".venv*/",
|
||||||
".vscode/",
|
".vscode/",
|
||||||
"bin/",
|
"bin/",
|
||||||
|
"app/core/bin/",
|
||||||
|
"app/common/bin/",
|
||||||
|
"app/proprietary/bin/",
|
||||||
"build/",
|
"build/",
|
||||||
|
"app/core/build/",
|
||||||
|
"app/common/build/",
|
||||||
|
"app/proprietary/build/",
|
||||||
"configs/",
|
"configs/",
|
||||||
|
"app/core/configs/",
|
||||||
"customFiles/",
|
"customFiles/",
|
||||||
|
"app/core/customFiles/",
|
||||||
"docs/",
|
"docs/",
|
||||||
"exampleYmlFiles",
|
"exampleYmlFiles",
|
||||||
"gradle/",
|
"gradle/",
|
||||||
@ -93,6 +101,9 @@
|
|||||||
".git-blame-ignore-revs",
|
".git-blame-ignore-revs",
|
||||||
".gitattributes",
|
".gitattributes",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
|
"app/core/.gitignore",
|
||||||
|
"app/common/.gitignore",
|
||||||
|
"app/proprietary/.gitignore",
|
||||||
".pre-commit-config.yaml"
|
".pre-commit-config.yaml"
|
||||||
],
|
],
|
||||||
"java.signatureHelp.enabled": true,
|
"java.signatureHelp.enabled": true,
|
||||||
|
@ -31,18 +31,12 @@ indent_size = 2
|
|||||||
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
[*.yaml]
|
[*.{yml,yaml}]
|
||||||
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.yml]
|
|
||||||
# YML files follow the same conventions as YAML files, using an indent size of 2 spaces.
|
|
||||||
indent_size = 2
|
|
||||||
insert_final_newline = false
|
|
||||||
trim_trailing_whitespace = false
|
|
||||||
|
|
||||||
[*.json]
|
[*.json]
|
||||||
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
23
.github/CODEOWNERS
vendored
23
.github/CODEOWNERS
vendored
@ -1,2 +1,21 @@
|
|||||||
# All PRs to V1 must be approved by Frooodle
|
# All PRs must be approved by Frooodle or Ludy87
|
||||||
* @Frooodle @reecebrowne @Ludy87 @DarioGii @ConnorYoh @EthanHealy01
|
* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh
|
||||||
|
|
||||||
|
#V1 frontend
|
||||||
|
/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
|
||||||
|
/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
|
||||||
|
|
||||||
|
#V2 frontend
|
||||||
|
/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle
|
||||||
|
|
||||||
|
#V2 docker
|
||||||
|
/docker/backend/** @Frooodle @Ludy87 @DarioGii @Ludy87
|
||||||
|
/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87
|
||||||
|
/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
|
||||||
|
|
||||||
|
|
||||||
|
#GHA (All users)
|
||||||
|
/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87
|
||||||
|
2
.github/config/.files.yaml
vendored
2
.github/config/.files.yaml
vendored
@ -27,3 +27,5 @@ project: &project
|
|||||||
- gradlew.bat
|
- gradlew.bat
|
||||||
- launch4jConfig.xml
|
- launch4jConfig.xml
|
||||||
- settings.gradle
|
- settings.gradle
|
||||||
|
- frontend/**
|
||||||
|
- docker/**
|
||||||
|
1
.github/labeler-config-srvaroa.yml
vendored
1
.github/labeler-config-srvaroa.yml
vendored
@ -78,6 +78,7 @@ labels:
|
|||||||
- 'app/core/src/main/resources/banner.txt'
|
- 'app/core/src/main/resources/banner.txt'
|
||||||
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
||||||
- 'app/core/src/main/resources/static/python/split_photos.py'
|
- 'app/core/src/main/resources/static/python/split_photos.py'
|
||||||
|
- 'app/core/src/main/resources/static/pipeline/defaultWebUIConfigs/**'
|
||||||
- 'application.properties'
|
- 'application.properties'
|
||||||
|
|
||||||
- label: 'Security'
|
- label: 'Security'
|
||||||
|
5
.github/labels.yml
vendored
5
.github/labels.yml
vendored
@ -42,6 +42,7 @@
|
|||||||
- name: "Front End"
|
- name: "Front End"
|
||||||
color: "BBD2F1"
|
color: "BBD2F1"
|
||||||
description: "Issues or pull requests related to front-end development"
|
description: "Issues or pull requests related to front-end development"
|
||||||
|
from_name: "frontend"
|
||||||
- name: "github-actions"
|
- name: "github-actions"
|
||||||
description: "Pull requests that update GitHub Actions code"
|
description: "Pull requests that update GitHub Actions code"
|
||||||
color: "999999"
|
color: "999999"
|
||||||
@ -77,6 +78,7 @@
|
|||||||
- name: "Translation"
|
- name: "Translation"
|
||||||
color: "9FABF9"
|
color: "9FABF9"
|
||||||
from_name: "translation"
|
from_name: "translation"
|
||||||
|
description: "Issues or pull requests related to translation"
|
||||||
- name: "upstream"
|
- name: "upstream"
|
||||||
color: "DEDEDE"
|
color: "DEDEDE"
|
||||||
- name: "v2"
|
- name: "v2"
|
||||||
@ -178,3 +180,6 @@
|
|||||||
- name: "pr-deployed"
|
- name: "pr-deployed"
|
||||||
color: "00FF00"
|
color: "00FF00"
|
||||||
description: "Pull request has been deployed to a test environment"
|
description: "Pull request has been deployed to a test environment"
|
||||||
|
- name: "codex"
|
||||||
|
color: "ededed"
|
||||||
|
description: "chatgpt AI generated code"
|
||||||
|
@ -42,7 +42,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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -153,7 +153,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
4
.github/workflows/ai_pr_title_review.yml
vendored
4
.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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ jobs:
|
|||||||
- name: AI PR Title Analysis
|
- name: AI PR Title Analysis
|
||||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
id: ai-title-analysis
|
id: ai-title-analysis
|
||||||
uses: actions/ai-inference@d645f067d89ee1d5d736a5990e327e504d1c5a4a # v1.1.0
|
uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt-file: ".github/config/system-prompt.txt"
|
system-prompt-file: ".github/config/system-prompt.txt"
|
||||||
|
2
.github/workflows/auto-labelerV2.yml
vendored
2
.github/workflows/auto-labelerV2.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
89
.github/workflows/build.yml
vendored
89
.github/workflows/build.yml
vendored
@ -5,6 +5,18 @@ on:
|
|||||||
branches: ["main", "V2", "V2-gha"]
|
branches: ["main", "V2", "V2-gha"]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -19,7 +31,9 @@ jobs:
|
|||||||
project: ${{ steps.changes.outputs.project }}
|
project: ${{ steps.changes.outputs.project }}
|
||||||
openapi: ${{ steps.changes.outputs.openapi }}
|
openapi: ${{ steps.changes.outputs.openapi }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Check for file changes
|
- name: Check for file changes
|
||||||
uses: dorny/paths-filter@v3.0.2
|
uses: dorny/paths-filter@v3.0.2
|
||||||
id: changes
|
id: changes
|
||||||
@ -38,7 +52,7 @@ jobs:
|
|||||||
spring-security: [true, false]
|
spring-security: [true, false]
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -104,6 +118,7 @@ jobs:
|
|||||||
- uses: gradle/actions/setup-gradle@v4.4.1
|
- uses: gradle/actions/setup-gradle@v4.4.1
|
||||||
- name: Generate OpenAPI documentation
|
- name: Generate OpenAPI documentation
|
||||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||||
|
|
||||||
- name: Upload OpenAPI Documentation
|
- name: Upload OpenAPI Documentation
|
||||||
uses: actions/upload-artifact@v4.6.2
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
@ -144,7 +159,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@ -185,7 +200,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -223,3 +238,69 @@ jobs:
|
|||||||
chmod +x ./testing/test.sh
|
chmod +x ./testing/test.sh
|
||||||
chmod +x ./testing/test_disabledEndpoints.sh
|
chmod +x ./testing/test_disabledEndpoints.sh
|
||||||
./testing/test.sh
|
./testing/test.sh
|
||||||
|
|
||||||
|
test-build-docker-images:
|
||||||
|
if: github.event_name == 'pull_request' && needs.files-changed.outputs.project == 'true'
|
||||||
|
needs: [files-changed, build, check-generateOpenApiDocs, check-licence]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||||
|
with:
|
||||||
|
gradle-version: 8.14
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: ./gradlew clean build
|
||||||
|
env:
|
||||||
|
DISABLE_ADDITIONAL_FEATURES: true
|
||||||
|
STIRLING_PDF_DESKTOP_UI: false
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.docker-rev }}
|
||||||
|
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
context: .
|
||||||
|
file: ./docker/backend/${{ matrix.docker-rev }}
|
||||||
|
push: false
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
|
- name: Upload Reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: reports-docker-${{ matrix.docker-rev }}
|
||||||
|
path: |
|
||||||
|
build/reports/tests/
|
||||||
|
build/test-results/
|
||||||
|
build/reports/problems/
|
||||||
|
retention-days: 3
|
||||||
|
if-no-files-found: warn
|
||||||
|
14
.github/workflows/check_properties.yml
vendored
14
.github/workflows/check_properties.yml
vendored
@ -6,6 +6,18 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "app/core/src/main/resources/messages_*.properties"
|
- "app/core/src/main/resources/messages_*.properties"
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # Allow read access to repository content
|
contents: read # Allow read access to repository content
|
||||||
|
|
||||||
@ -18,7 +30,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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
14
.github/workflows/licenses-update.yml
vendored
14
.github/workflows/licenses-update.yml
vendored
@ -7,6 +7,18 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "build.gradle"
|
- "build.gradle"
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -19,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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
14
.github/workflows/multiOSReleases.yml
vendored
14
.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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@ -301,7 +301,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
16
.github/workflows/pre_commit.yml
vendored
16
.github/workflows/pre_commit.yml
vendored
@ -2,8 +2,9 @@ name: Pre-commit
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
push:
|
||||||
- cron: "0 0 * * 1"
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@ -16,7 +17,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -46,6 +47,15 @@ jobs:
|
|||||||
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Set up JDK
|
||||||
|
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||||
|
with:
|
||||||
|
java-version: 17
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew clean build
|
||||||
|
|
||||||
- name: git add
|
- name: git add
|
||||||
run: |
|
run: |
|
||||||
git add .
|
git add .
|
||||||
|
26
.github/workflows/push-docker.yml
vendored
26
.github/workflows/push-docker.yml
vendored
@ -7,6 +7,18 @@ on:
|
|||||||
- master
|
- master
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -18,7 +30,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -42,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
@ -55,13 +67,13 @@ jobs:
|
|||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@ -76,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@ -122,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Generate tags ultra-lite
|
||||||
id: meta2
|
id: meta2
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@ -153,7 +165,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
8
.github/workflows/releaseArtifacts.yml
vendored
8
.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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -95,7 +95,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
run: cosign generate-key-pair
|
run: cosign generate-key-pair
|
||||||
@ -161,7 +161,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
4
.github/workflows/scorecards.yml
vendored
4
.github/workflows/scorecards.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
uses: github/codeql-action/upload-sarif@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
14
.github/workflows/sonarqube.yml
vendored
14
.github/workflows/sonarqube.yml
vendored
@ -9,6 +9,18 @@ on:
|
|||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
actions: read
|
actions: read
|
||||||
@ -18,7 +30,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
14
.github/workflows/swagger.yml
vendored
14
.github/workflows/swagger.yml
vendored
@ -6,6 +6,18 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -14,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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
14
.github/workflows/sync_files.yml
vendored
14
.github/workflows/sync_files.yml
vendored
@ -12,6 +12,18 @@ on:
|
|||||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||||
- "scripts/ignore_translation.toml"
|
- "scripts/ignore_translation.toml"
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -20,7 +32,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
20
.github/workflows/testdriver.yml
vendored
20
.github/workflows/testdriver.yml
vendored
@ -4,6 +4,18 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: ["master", "UITest", "testdriver"]
|
branches: ["master", "UITest", "testdriver"]
|
||||||
|
|
||||||
|
# cancel in-progress jobs if a new job is triggered
|
||||||
|
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||||
|
# or a pull request is updated.
|
||||||
|
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||||
|
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||||
|
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||||
|
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||||
|
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
@ -12,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@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -45,7 +57,7 @@ jobs:
|
|||||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
@ -110,7 +122,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@ -144,7 +156,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -125,10 +125,10 @@ SwaggerDoc.json
|
|||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
/build
|
build
|
||||||
/app/core/build
|
app/core/build
|
||||||
/app/common/build
|
app/common/build
|
||||||
/app/proprietary/build
|
app/proprietary/build
|
||||||
common/build
|
common/build
|
||||||
proprietary/build
|
proprietary/build
|
||||||
stirling-pdf/build
|
stirling-pdf/build
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.12.0
|
rev: v0.12.7
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -22,7 +22,7 @@ repos:
|
|||||||
files: \.(html|css|js|py|md)$
|
files: \.(html|css|js|py|md)$
|
||||||
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
exclude: (.vscode|.devcontainer|app/core/src/main/resources|app/proprietary/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.27.2
|
rev: v8.28.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -2,6 +2,7 @@
|
|||||||
"editor.wordSegmenterLocales": "",
|
"editor.wordSegmenterLocales": "",
|
||||||
"editor.guides.bracketPairs": "active",
|
"editor.guides.bracketPairs": "active",
|
||||||
"editor.guides.bracketPairsHorizontal": "active",
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
|
"editor.defaultFormatter": "EditorConfig.EditorConfig",
|
||||||
"cSpell.enabled": false,
|
"cSpell.enabled": false,
|
||||||
"[feature]": {
|
"[feature]": {
|
||||||
"editor.defaultFormatter": "alexkrechik.cucumberautocomplete"
|
"editor.defaultFormatter": "alexkrechik.cucumberautocomplete"
|
||||||
@ -40,7 +41,7 @@
|
|||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
"java.format.enabled": true,
|
"java.format.enabled": true,
|
||||||
"java.format.settings.profile": "GoogleStyle",
|
"java.format.settings.profile": "GoogleStyle",
|
||||||
"java.format.settings.google.version": "1.27.0",
|
"java.format.settings.google.version": "1.28.0",
|
||||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||||
// (DE) Aktiviert Kommentare im Java-Format.
|
// (DE) Aktiviert Kommentare im Java-Format.
|
||||||
// (EN) Enables comments in Java formatting.
|
// (EN) Enables comments in Java formatting.
|
||||||
|
74
README.md
74
README.md
@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
|
|||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| -------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||||
|
|
||||||
## Stirling PDF Enterprise
|
## Stirling PDF Enterprise
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ bootRun {
|
|||||||
}
|
}
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
target sourceSets.main.allJava
|
target 'src/**/java/**/*.java'
|
||||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||||
|
|
||||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||||
@ -13,6 +13,18 @@ spotless {
|
|||||||
leadingTabsToSpaces()
|
leadingTabsToSpaces()
|
||||||
endWithNewline()
|
endWithNewline()
|
||||||
}
|
}
|
||||||
|
yaml {
|
||||||
|
target '**/*.yml', '**/*.yaml'
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
leadingTabsToSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
format 'gradle', {
|
||||||
|
target '**/gradle/*.gradle', '**/*.gradle'
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
leadingTabsToSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.springframework.boot:spring-boot-starter-web'
|
api 'org.springframework.boot:spring-boot-starter-web'
|
||||||
@ -29,5 +41,5 @@ dependencies {
|
|||||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
||||||
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
||||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.3'
|
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.4'
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ public class InstallationPathConfig {
|
|||||||
private static final String CUSTOM_FILES_PATH;
|
private static final String CUSTOM_FILES_PATH;
|
||||||
private static final String CLIENT_WEBUI_PATH;
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
private static final String SCRIPTS_PATH;
|
private static final String SCRIPTS_PATH;
|
||||||
|
private static final String PIPELINE_PATH;
|
||||||
|
|
||||||
// Config paths
|
// Config paths
|
||||||
private static final String SETTINGS_PATH;
|
private static final String SETTINGS_PATH;
|
||||||
@ -33,6 +34,7 @@ public class InstallationPathConfig {
|
|||||||
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
||||||
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
||||||
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
||||||
|
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
|
||||||
|
|
||||||
// Initialize config paths
|
// Initialize config paths
|
||||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
@ -95,6 +97,10 @@ public class InstallationPathConfig {
|
|||||||
return SCRIPTS_PATH;
|
return SCRIPTS_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getPipelinePath() {
|
||||||
|
return PIPELINE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getSettingsPath() {
|
public static String getSettingsPath() {
|
||||||
return SETTINGS_PATH;
|
return SETTINGS_PATH;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,9 @@ import org.springframework.core.io.Resource;
|
|||||||
import org.springframework.core.io.support.EncodedResource;
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
@ -58,7 +61,10 @@ public class ApplicationProperties {
|
|||||||
private Mail mail = new Mail();
|
private Mail mail = new Mail();
|
||||||
|
|
||||||
private Premium premium = new Premium();
|
private Premium premium = new Premium();
|
||||||
|
|
||||||
|
@JsonIgnore // Deprecated - completely hidden from JSON serialization
|
||||||
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
||||||
|
|
||||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||||
private ProcessExecutor processExecutor = new ProcessExecutor();
|
private ProcessExecutor processExecutor = new ProcessExecutor();
|
||||||
|
|
||||||
@ -168,17 +174,30 @@ public class ApplicationProperties {
|
|||||||
private Boolean autoCreateUser = false;
|
private Boolean autoCreateUser = false;
|
||||||
private Boolean blockRegistration = false;
|
private Boolean blockRegistration = false;
|
||||||
private String registrationId = "stirling";
|
private String registrationId = "stirling";
|
||||||
@ToString.Exclude private String idpMetadataUri;
|
|
||||||
|
@ToString.Exclude
|
||||||
|
@JsonProperty("idpMetadataUri")
|
||||||
|
private String idpMetadataUri;
|
||||||
|
|
||||||
private String idpSingleLogoutUrl;
|
private String idpSingleLogoutUrl;
|
||||||
private String idpSingleLoginUrl;
|
private String idpSingleLoginUrl;
|
||||||
private String idpIssuer;
|
private String idpIssuer;
|
||||||
private String idpCert;
|
|
||||||
@ToString.Exclude private String privateKey;
|
|
||||||
@ToString.Exclude private String spCert;
|
|
||||||
|
|
||||||
|
@JsonProperty("idpCert")
|
||||||
|
private String idpCert;
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
@JsonProperty("privateKey")
|
||||||
|
private String privateKey;
|
||||||
|
|
||||||
|
@ToString.Exclude
|
||||||
|
@JsonProperty("spCert")
|
||||||
|
private String spCert;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public InputStream getIdpMetadataUri() throws IOException {
|
public InputStream getIdpMetadataUri() throws IOException {
|
||||||
if (idpMetadataUri.startsWith("classpath:")) {
|
if (idpMetadataUri.startsWith("classpath:")) {
|
||||||
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
|
return new ClassPathResource(idpMetadataUri.substring("classpath:".length()))
|
||||||
.getInputStream();
|
.getInputStream();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -192,6 +211,7 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public Resource getSpCert() {
|
public Resource getSpCert() {
|
||||||
if (spCert == null) return null;
|
if (spCert == null) return null;
|
||||||
if (spCert.startsWith("classpath:")) {
|
if (spCert.startsWith("classpath:")) {
|
||||||
@ -201,6 +221,7 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public Resource getIdpCert() {
|
public Resource getIdpCert() {
|
||||||
if (idpCert == null) return null;
|
if (idpCert == null) return null;
|
||||||
if (idpCert.startsWith("classpath:")) {
|
if (idpCert.startsWith("classpath:")) {
|
||||||
@ -210,7 +231,9 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public Resource getPrivateKey() {
|
public Resource getPrivateKey() {
|
||||||
|
if (privateKey == null) return null;
|
||||||
if (privateKey.startsWith("classpath:")) {
|
if (privateKey.startsWith("classpath:")) {
|
||||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
||||||
} else {
|
} else {
|
||||||
@ -289,7 +312,9 @@ public class ApplicationProperties {
|
|||||||
private Boolean enableAnalytics;
|
private Boolean enableAnalytics;
|
||||||
private Datasource datasource;
|
private Datasource datasource;
|
||||||
private Boolean disableSanitize;
|
private Boolean disableSanitize;
|
||||||
|
private int maxDPI;
|
||||||
private Boolean enableUrlToPDF;
|
private Boolean enableUrlToPDF;
|
||||||
|
private Html html = new Html();
|
||||||
private CustomPaths customPaths = new CustomPaths();
|
private CustomPaths customPaths = new CustomPaths();
|
||||||
private String fileUploadLimit;
|
private String fileUploadLimit;
|
||||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||||
@ -320,8 +345,12 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class TempFileManagement {
|
public static class TempFileManagement {
|
||||||
|
@JsonProperty("baseTmpDir")
|
||||||
private String baseTmpDir = "";
|
private String baseTmpDir = "";
|
||||||
|
|
||||||
|
@JsonProperty("libreofficeDir")
|
||||||
private String libreofficeDir = "";
|
private String libreofficeDir = "";
|
||||||
|
|
||||||
private String systemTempDir = "";
|
private String systemTempDir = "";
|
||||||
private String prefix = "stirling-pdf-";
|
private String prefix = "stirling-pdf-";
|
||||||
private long maxAgeHours = 24;
|
private long maxAgeHours = 24;
|
||||||
@ -329,12 +358,14 @@ public class ApplicationProperties {
|
|||||||
private boolean startupCleanup = true;
|
private boolean startupCleanup = true;
|
||||||
private boolean cleanupSystemTemp = false;
|
private boolean cleanupSystemTemp = false;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public String getBaseTmpDir() {
|
public String getBaseTmpDir() {
|
||||||
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
return baseTmpDir != null && !baseTmpDir.isEmpty()
|
||||||
? baseTmpDir
|
? baseTmpDir
|
||||||
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
|
: java.lang.System.getProperty("java.io.tmpdir") + "/stirling-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
public String getLibreofficeDir() {
|
public String getLibreofficeDir() {
|
||||||
return libreofficeDir != null && !libreofficeDir.isEmpty()
|
return libreofficeDir != null && !libreofficeDir.isEmpty()
|
||||||
? libreofficeDir
|
? libreofficeDir
|
||||||
@ -342,6 +373,25 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Html {
|
||||||
|
private UrlSecurity urlSecurity = new UrlSecurity();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class UrlSecurity {
|
||||||
|
private boolean enabled = true;
|
||||||
|
private String level = "MEDIUM"; // MAX, MEDIUM, OFF
|
||||||
|
private List<String> allowedDomains = new ArrayList<>();
|
||||||
|
private List<String> blockedDomains = new ArrayList<>();
|
||||||
|
private List<String> internalTlds =
|
||||||
|
Arrays.asList(".local", ".internal", ".corp", ".home");
|
||||||
|
private boolean blockPrivateNetworks = true;
|
||||||
|
private boolean blockLocalhost = true;
|
||||||
|
private boolean blockLinkLocal = true;
|
||||||
|
private boolean blockCloudMetadata = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Datasource {
|
public static class Datasource {
|
||||||
private boolean enableCustomDatabase;
|
private boolean enableCustomDatabase;
|
||||||
@ -591,12 +641,24 @@ public class ApplicationProperties {
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class TimeoutMinutes {
|
public static class TimeoutMinutes {
|
||||||
|
@JsonProperty("libreOfficetimeoutMinutes")
|
||||||
private long libreOfficeTimeoutMinutes;
|
private long libreOfficeTimeoutMinutes;
|
||||||
|
|
||||||
|
@JsonProperty("pdfToHtmltimeoutMinutes")
|
||||||
private long pdfToHtmlTimeoutMinutes;
|
private long pdfToHtmlTimeoutMinutes;
|
||||||
|
|
||||||
|
@JsonProperty("pythonOpenCvtimeoutMinutes")
|
||||||
private long pythonOpenCvTimeoutMinutes;
|
private long pythonOpenCvTimeoutMinutes;
|
||||||
|
|
||||||
|
@JsonProperty("weasyPrinttimeoutMinutes")
|
||||||
private long weasyPrintTimeoutMinutes;
|
private long weasyPrintTimeoutMinutes;
|
||||||
|
|
||||||
|
@JsonProperty("installApptimeoutMinutes")
|
||||||
private long installAppTimeoutMinutes;
|
private long installAppTimeoutMinutes;
|
||||||
|
|
||||||
|
@JsonProperty("calibretimeoutMinutes")
|
||||||
private long calibreTimeoutMinutes;
|
private long calibreTimeoutMinutes;
|
||||||
|
|
||||||
private long tesseractTimeoutMinutes;
|
private long tesseractTimeoutMinutes;
|
||||||
private long qpdfTimeoutMinutes;
|
private long qpdfTimeoutMinutes;
|
||||||
private long ghostscriptTimeoutMinutes;
|
private long ghostscriptTimeoutMinutes;
|
||||||
|
@ -0,0 +1,208 @@
|
|||||||
|
package stirling.software.common.service;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class SsrfProtectionService {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Pattern DATA_URL_PATTERN =
|
||||||
|
Pattern.compile("^data:.*", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern FRAGMENT_PATTERN = Pattern.compile("^#.*");
|
||||||
|
|
||||||
|
public enum SsrfProtectionLevel {
|
||||||
|
OFF, // No SSRF protection - allows all URLs
|
||||||
|
MEDIUM, // Block internal networks but allow external URLs
|
||||||
|
MAX // Block all external URLs - only data: and fragments
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUrlAllowed(String url) {
|
||||||
|
ApplicationProperties.Html.UrlSecurity config =
|
||||||
|
applicationProperties.getSystem().getHtml().getUrlSecurity();
|
||||||
|
|
||||||
|
if (!config.isEnabled()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmedUrl = url.trim();
|
||||||
|
|
||||||
|
// Always allow data URLs and fragments
|
||||||
|
if (DATA_URL_PATTERN.matcher(trimmedUrl).matches()
|
||||||
|
|| FRAGMENT_PATTERN.matcher(trimmedUrl).matches()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SsrfProtectionLevel level = parseProtectionLevel(config.getLevel());
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case OFF:
|
||||||
|
return true;
|
||||||
|
case MAX:
|
||||||
|
return isMaxSecurityAllowed(trimmedUrl, config);
|
||||||
|
case MEDIUM:
|
||||||
|
return isMediumSecurityAllowed(trimmedUrl, config);
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SsrfProtectionLevel parseProtectionLevel(String level) {
|
||||||
|
try {
|
||||||
|
return SsrfProtectionLevel.valueOf(level.toUpperCase());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Invalid SSRF protection level '{}', defaulting to MEDIUM", level);
|
||||||
|
return SsrfProtectionLevel.MEDIUM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMaxSecurityAllowed(
|
||||||
|
String url, ApplicationProperties.Html.UrlSecurity config) {
|
||||||
|
// MAX security: only allow explicitly whitelisted domains
|
||||||
|
try {
|
||||||
|
URI uri = new URI(url);
|
||||||
|
String host = uri.getHost();
|
||||||
|
|
||||||
|
if (host == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.getAllowedDomains().contains(host.toLowerCase());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to parse URL for MAX security check: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMediumSecurityAllowed(
|
||||||
|
String url, ApplicationProperties.Html.UrlSecurity config) {
|
||||||
|
try {
|
||||||
|
URI uri = new URI(url);
|
||||||
|
String host = uri.getHost();
|
||||||
|
|
||||||
|
if (host == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String hostLower = host.toLowerCase();
|
||||||
|
|
||||||
|
// Check explicit blocked domains
|
||||||
|
if (config.getBlockedDomains().contains(hostLower)) {
|
||||||
|
log.debug("URL blocked by explicit domain blocklist: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check internal TLD patterns
|
||||||
|
for (String tld : config.getInternalTlds()) {
|
||||||
|
if (hostLower.endsWith(tld.toLowerCase())) {
|
||||||
|
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If allowedDomains is specified, only allow those
|
||||||
|
if (!config.getAllowedDomains().isEmpty()) {
|
||||||
|
boolean isAllowed =
|
||||||
|
config.getAllowedDomains().stream()
|
||||||
|
.anyMatch(
|
||||||
|
domain ->
|
||||||
|
hostLower.equals(domain.toLowerCase())
|
||||||
|
|| hostLower.endsWith(
|
||||||
|
"." + domain.toLowerCase()));
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
log.debug("URL not in allowed domains list: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve hostname to IP address for network-based checks
|
||||||
|
try {
|
||||||
|
InetAddress address = InetAddress.getByName(host);
|
||||||
|
|
||||||
|
if (config.isBlockPrivateNetworks() && isPrivateAddress(address)) {
|
||||||
|
log.debug("URL blocked - private network address: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isBlockLocalhost() && address.isLoopbackAddress()) {
|
||||||
|
log.debug("URL blocked - localhost address: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isBlockLinkLocal() && address.isLinkLocalAddress()) {
|
||||||
|
log.debug("URL blocked - link-local address: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.isBlockCloudMetadata()
|
||||||
|
&& isCloudMetadataAddress(address.getHostAddress())) {
|
||||||
|
log.debug("URL blocked - cloud metadata endpoint: {}", url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
log.debug("Failed to resolve hostname for SSRF check: {}", host, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Failed to parse URL for MEDIUM security check: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPrivateAddress(InetAddress address) {
|
||||||
|
return address.isSiteLocalAddress()
|
||||||
|
|| address.isAnyLocalAddress()
|
||||||
|
|| isPrivateIPv4Range(address.getHostAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPrivateIPv4Range(String ip) {
|
||||||
|
return ip.startsWith("10.")
|
||||||
|
|| ip.startsWith("192.168.")
|
||||||
|
|| (ip.startsWith("172.") && isInRange172(ip))
|
||||||
|
|| ip.startsWith("127.")
|
||||||
|
|| "0.0.0.0".equals(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isInRange172(String ip) {
|
||||||
|
String[] parts = ip.split("\\.");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
try {
|
||||||
|
int secondOctet = Integer.parseInt(parts[1]);
|
||||||
|
return secondOctet >= 16 && secondOctet <= 31;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCloudMetadataAddress(String ip) {
|
||||||
|
// Cloud metadata endpoints for AWS, GCP, Azure, Oracle Cloud, and IBM Cloud
|
||||||
|
return ip.startsWith("169.254.169.254") // AWS/GCP/Azure
|
||||||
|
|| ip.startsWith("fd00:ec2::254") // AWS IPv6
|
||||||
|
|| ip.startsWith("169.254.169.253") // Oracle Cloud
|
||||||
|
|| ip.startsWith("169.254.169.250"); // IBM Cloud
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,71 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import org.owasp.html.AttributePolicy;
|
||||||
import org.owasp.html.HtmlPolicyBuilder;
|
import org.owasp.html.HtmlPolicyBuilder;
|
||||||
import org.owasp.html.PolicyFactory;
|
import org.owasp.html.PolicyFactory;
|
||||||
import org.owasp.html.Sanitizers;
|
import org.owasp.html.Sanitizers;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.service.SsrfProtectionService;
|
||||||
|
|
||||||
|
@Component
|
||||||
public class CustomHtmlSanitizer {
|
public class CustomHtmlSanitizer {
|
||||||
private static final PolicyFactory POLICY =
|
|
||||||
|
private final SsrfProtectionService ssrfProtectionService;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CustomHtmlSanitizer(
|
||||||
|
SsrfProtectionService ssrfProtectionService,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.ssrfProtectionService = ssrfProtectionService;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final AttributePolicy SSRF_SAFE_URL_POLICY =
|
||||||
|
new AttributePolicy() {
|
||||||
|
@Override
|
||||||
|
public String apply(String elementName, String attributeName, String value) {
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String trimmedValue = value.trim();
|
||||||
|
|
||||||
|
// Use the SSRF protection service to validate the URL
|
||||||
|
if (ssrfProtectionService != null
|
||||||
|
&& !ssrfProtectionService.isUrlAllowed(trimmedValue)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimmedValue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final PolicyFactory SSRF_SAFE_IMAGES_POLICY =
|
||||||
|
new HtmlPolicyBuilder()
|
||||||
|
.allowElements("img")
|
||||||
|
.allowAttributes("alt", "width", "height", "title")
|
||||||
|
.onElements("img")
|
||||||
|
.allowAttributes("src")
|
||||||
|
.matching(SSRF_SAFE_URL_POLICY)
|
||||||
|
.onElements("img")
|
||||||
|
.toFactory();
|
||||||
|
|
||||||
|
private final PolicyFactory POLICY =
|
||||||
Sanitizers.FORMATTING
|
Sanitizers.FORMATTING
|
||||||
.and(Sanitizers.BLOCKS)
|
.and(Sanitizers.BLOCKS)
|
||||||
.and(Sanitizers.STYLES)
|
.and(Sanitizers.STYLES)
|
||||||
.and(Sanitizers.LINKS)
|
.and(Sanitizers.LINKS)
|
||||||
.and(Sanitizers.TABLES)
|
.and(Sanitizers.TABLES)
|
||||||
.and(Sanitizers.IMAGES)
|
.and(SSRF_SAFE_IMAGES_POLICY)
|
||||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||||
|
|
||||||
public static String sanitize(String html) {
|
public String sanitize(String html) {
|
||||||
String htmlAfter = POLICY.sanitize(html);
|
boolean disableSanitize =
|
||||||
return htmlAfter;
|
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
||||||
|
return disableSanitize ? html : POLICY.sanitize(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,652 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class EmlParser {
|
||||||
|
|
||||||
|
private static volatile Boolean jakartaMailAvailable = null;
|
||||||
|
private static volatile Method mimeUtilityDecodeTextMethod = null;
|
||||||
|
private static volatile boolean mimeUtilityChecked = false;
|
||||||
|
|
||||||
|
private static final Pattern MIME_ENCODED_PATTERN =
|
||||||
|
Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||||
|
|
||||||
|
private static final String DISPOSITION_ATTACHMENT = "attachment";
|
||||||
|
private static final String TEXT_PLAIN = "text/plain";
|
||||||
|
private static final String TEXT_HTML = "text/html";
|
||||||
|
private static final String MULTIPART_PREFIX = "multipart/";
|
||||||
|
|
||||||
|
private static final String HEADER_CONTENT_TYPE = "content-type:";
|
||||||
|
private static final String HEADER_CONTENT_DISPOSITION = "content-disposition:";
|
||||||
|
private static final String HEADER_CONTENT_TRANSFER_ENCODING = "content-transfer-encoding:";
|
||||||
|
private static final String HEADER_CONTENT_ID = "Content-ID";
|
||||||
|
private static final String HEADER_SUBJECT = "Subject:";
|
||||||
|
private static final String HEADER_FROM = "From:";
|
||||||
|
private static final String HEADER_TO = "To:";
|
||||||
|
private static final String HEADER_CC = "Cc:";
|
||||||
|
private static final String HEADER_BCC = "Bcc:";
|
||||||
|
private static final String HEADER_DATE = "Date:";
|
||||||
|
|
||||||
|
private static synchronized boolean isJakartaMailAvailable() {
|
||||||
|
if (jakartaMailAvailable == null) {
|
||||||
|
try {
|
||||||
|
Class.forName("jakarta.mail.internet.MimeMessage");
|
||||||
|
Class.forName("jakarta.mail.Session");
|
||||||
|
Class.forName("jakarta.mail.internet.MimeUtility");
|
||||||
|
Class.forName("jakarta.mail.internet.MimePart");
|
||||||
|
Class.forName("jakarta.mail.internet.MimeMultipart");
|
||||||
|
Class.forName("jakarta.mail.Multipart");
|
||||||
|
Class.forName("jakarta.mail.Part");
|
||||||
|
jakartaMailAvailable = true;
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
jakartaMailAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jakartaMailAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EmailContent extractEmailContent(
|
||||||
|
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer)
|
||||||
|
throws IOException {
|
||||||
|
EmlProcessingUtils.validateEmlInput(emlBytes);
|
||||||
|
|
||||||
|
if (isJakartaMailAvailable()) {
|
||||||
|
return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer);
|
||||||
|
} else {
|
||||||
|
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailContent extractEmailContentBasic(
|
||||||
|
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
String emlContent = new String(emlBytes, StandardCharsets.UTF_8);
|
||||||
|
EmailContent content = new EmailContent();
|
||||||
|
|
||||||
|
content.setSubject(extractBasicHeader(emlContent, HEADER_SUBJECT));
|
||||||
|
content.setFrom(extractBasicHeader(emlContent, HEADER_FROM));
|
||||||
|
content.setTo(extractBasicHeader(emlContent, HEADER_TO));
|
||||||
|
content.setCc(extractBasicHeader(emlContent, HEADER_CC));
|
||||||
|
content.setBcc(extractBasicHeader(emlContent, HEADER_BCC));
|
||||||
|
|
||||||
|
String dateStr = extractBasicHeader(emlContent, HEADER_DATE);
|
||||||
|
if (!dateStr.isEmpty()) {
|
||||||
|
content.setDateString(dateStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
String htmlBody = extractHtmlBody(emlContent);
|
||||||
|
if (htmlBody != null) {
|
||||||
|
content.setHtmlBody(htmlBody);
|
||||||
|
} else {
|
||||||
|
String textBody = extractTextBody(emlContent);
|
||||||
|
content.setTextBody(textBody != null ? textBody : "Email content could not be parsed");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.getAttachments().addAll(extractAttachmentsBasic(emlContent));
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailContent extractEmailContentAdvanced(
|
||||||
|
byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
try {
|
||||||
|
Class<?> sessionClass = Class.forName("jakarta.mail.Session");
|
||||||
|
Class<?> mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage");
|
||||||
|
|
||||||
|
Method getDefaultInstance =
|
||||||
|
sessionClass.getMethod("getDefaultInstance", Properties.class);
|
||||||
|
Object session = getDefaultInstance.invoke(null, new Properties());
|
||||||
|
|
||||||
|
Class<?>[] constructorArgs = new Class<?>[] {sessionClass, InputStream.class};
|
||||||
|
Constructor<?> mimeMessageConstructor =
|
||||||
|
mimeMessageClass.getConstructor(constructorArgs);
|
||||||
|
Object message =
|
||||||
|
mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes));
|
||||||
|
|
||||||
|
return extractFromMimeMessage(message, request, customHtmlSanitizer);
|
||||||
|
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmailContent extractFromMimeMessage(
|
||||||
|
Object message, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
EmailContent content = new EmailContent();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> messageClass = message.getClass();
|
||||||
|
|
||||||
|
Method getSubject = messageClass.getMethod("getSubject");
|
||||||
|
String subject = (String) getSubject.invoke(message);
|
||||||
|
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
|
||||||
|
|
||||||
|
Method getFrom = messageClass.getMethod("getFrom");
|
||||||
|
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
|
||||||
|
content.setFrom(buildAddressString(fromAddresses));
|
||||||
|
|
||||||
|
extractRecipients(message, messageClass, content);
|
||||||
|
|
||||||
|
Method getSentDate = messageClass.getMethod("getSentDate");
|
||||||
|
content.setDate((Date) getSentDate.invoke(message));
|
||||||
|
|
||||||
|
Method getContent = messageClass.getMethod("getContent");
|
||||||
|
Object messageContent = getContent.invoke(message);
|
||||||
|
|
||||||
|
processMessageContent(message, messageContent, content, request, customHtmlSanitizer);
|
||||||
|
|
||||||
|
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||||
|
content.setSubject("Email Conversion");
|
||||||
|
content.setFrom("Unknown");
|
||||||
|
content.setTo("Unknown");
|
||||||
|
content.setCc("");
|
||||||
|
content.setBcc("");
|
||||||
|
content.setTextBody("Email content could not be parsed with advanced processing");
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void extractRecipients(
|
||||||
|
Object message, Class<?> messageClass, EmailContent content) {
|
||||||
|
try {
|
||||||
|
Method getRecipients =
|
||||||
|
messageClass.getMethod(
|
||||||
|
"getRecipients", Class.forName("jakarta.mail.Message$RecipientType"));
|
||||||
|
Class<?> recipientTypeClass = Class.forName("jakarta.mail.Message$RecipientType");
|
||||||
|
|
||||||
|
Object toType = recipientTypeClass.getField("TO").get(null);
|
||||||
|
Object[] toRecipients = (Object[]) getRecipients.invoke(message, toType);
|
||||||
|
content.setTo(buildAddressString(toRecipients));
|
||||||
|
|
||||||
|
Object ccType = recipientTypeClass.getField("CC").get(null);
|
||||||
|
Object[] ccRecipients = (Object[]) getRecipients.invoke(message, ccType);
|
||||||
|
content.setCc(buildAddressString(ccRecipients));
|
||||||
|
|
||||||
|
Object bccType = recipientTypeClass.getField("BCC").get(null);
|
||||||
|
Object[] bccRecipients = (Object[]) getRecipients.invoke(message, bccType);
|
||||||
|
content.setBcc(buildAddressString(bccRecipients));
|
||||||
|
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
try {
|
||||||
|
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
|
||||||
|
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
|
||||||
|
content.setTo(buildAddressString(recipients));
|
||||||
|
content.setCc("");
|
||||||
|
content.setBcc("");
|
||||||
|
} catch (ReflectiveOperationException ex) {
|
||||||
|
content.setTo("");
|
||||||
|
content.setCc("");
|
||||||
|
content.setBcc("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildAddressString(Object[] addresses) {
|
||||||
|
if (addresses == null || addresses.length == 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < addresses.length; i++) {
|
||||||
|
if (i > 0) builder.append(", ");
|
||||||
|
builder.append(safeMimeDecode(addresses[i].toString()));
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processMessageContent(
|
||||||
|
Object message,
|
||||||
|
Object messageContent,
|
||||||
|
EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
try {
|
||||||
|
if (messageContent instanceof String stringContent) {
|
||||||
|
Method getContentType = message.getClass().getMethod("getContentType");
|
||||||
|
String contentType = (String) getContentType.invoke(message);
|
||||||
|
|
||||||
|
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
|
||||||
|
content.setHtmlBody(stringContent);
|
||||||
|
} else {
|
||||||
|
content.setTextBody(stringContent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
|
||||||
|
if (multipartClass.isInstance(messageContent)) {
|
||||||
|
processMultipart(messageContent, content, request, customHtmlSanitizer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException | ClassCastException e) {
|
||||||
|
content.setTextBody("Email content could not be parsed with advanced processing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processMultipart(
|
||||||
|
Object multipart,
|
||||||
|
EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer,
|
||||||
|
int depth) {
|
||||||
|
|
||||||
|
final int MAX_MULTIPART_DEPTH = 10;
|
||||||
|
if (depth > MAX_MULTIPART_DEPTH) {
|
||||||
|
content.setHtmlBody("<div class=\"error\">Maximum multipart depth exceeded</div>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Class<?> multipartClass = multipart.getClass();
|
||||||
|
Method getCount = multipartClass.getMethod("getCount");
|
||||||
|
int count = (Integer) getCount.invoke(multipart);
|
||||||
|
|
||||||
|
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
Object part = getBodyPart.invoke(multipart, i);
|
||||||
|
processPart(part, content, request, customHtmlSanitizer, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ReflectiveOperationException | ClassCastException e) {
|
||||||
|
content.setHtmlBody("<div class=\"error\">Error processing multipart content</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processPart(
|
||||||
|
Object part,
|
||||||
|
EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer,
|
||||||
|
int depth) {
|
||||||
|
try {
|
||||||
|
Class<?> partClass = part.getClass();
|
||||||
|
|
||||||
|
Method isMimeType = partClass.getMethod("isMimeType", String.class);
|
||||||
|
Method getContent = partClass.getMethod("getContent");
|
||||||
|
Method getDisposition = partClass.getMethod("getDisposition");
|
||||||
|
Method getFileName = partClass.getMethod("getFileName");
|
||||||
|
Method getContentType = partClass.getMethod("getContentType");
|
||||||
|
Method getHeader = partClass.getMethod("getHeader", String.class);
|
||||||
|
|
||||||
|
Object disposition = getDisposition.invoke(part);
|
||||||
|
String filename = (String) getFileName.invoke(part);
|
||||||
|
String contentType = (String) getContentType.invoke(part);
|
||||||
|
|
||||||
|
String normalizedDisposition =
|
||||||
|
disposition != null ? ((String) disposition).toLowerCase() : null;
|
||||||
|
|
||||||
|
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
|
||||||
|
Object partContent = getContent.invoke(part);
|
||||||
|
if (partContent instanceof String stringContent) {
|
||||||
|
content.setTextBody(stringContent);
|
||||||
|
}
|
||||||
|
} else if ((Boolean) isMimeType.invoke(part, TEXT_HTML)
|
||||||
|
&& normalizedDisposition == null) {
|
||||||
|
Object partContent = getContent.invoke(part);
|
||||||
|
if (partContent instanceof String stringContent) {
|
||||||
|
String htmlBody =
|
||||||
|
customHtmlSanitizer != null
|
||||||
|
? customHtmlSanitizer.sanitize(stringContent)
|
||||||
|
: stringContent;
|
||||||
|
content.setHtmlBody(htmlBody);
|
||||||
|
}
|
||||||
|
} else if ((normalizedDisposition != null
|
||||||
|
&& normalizedDisposition.contains(DISPOSITION_ATTACHMENT))
|
||||||
|
|| (filename != null && !filename.trim().isEmpty())) {
|
||||||
|
|
||||||
|
processAttachment(
|
||||||
|
part, content, request, getHeader, getContent, filename, contentType);
|
||||||
|
} else if ((Boolean) isMimeType.invoke(part, "multipart/*")) {
|
||||||
|
Object multipartContent = getContent.invoke(part);
|
||||||
|
if (multipartContent != null) {
|
||||||
|
Class<?> multipartClass = Class.forName("jakarta.mail.Multipart");
|
||||||
|
if (multipartClass.isInstance(multipartContent)) {
|
||||||
|
processMultipart(
|
||||||
|
multipartContent, content, request, customHtmlSanitizer, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||||
|
// Continue processing other parts if one fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void processAttachment(
|
||||||
|
Object part,
|
||||||
|
EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
Method getHeader,
|
||||||
|
Method getContent,
|
||||||
|
String filename,
|
||||||
|
String contentType) {
|
||||||
|
|
||||||
|
content.setAttachmentCount(content.getAttachmentCount() + 1);
|
||||||
|
|
||||||
|
if (filename != null && !filename.trim().isEmpty()) {
|
||||||
|
EmailAttachment attachment = new EmailAttachment();
|
||||||
|
attachment.setFilename(safeMimeDecode(filename));
|
||||||
|
attachment.setContentType(contentType);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String[] contentIdHeaders = (String[]) getHeader.invoke(part, HEADER_CONTENT_ID);
|
||||||
|
if (contentIdHeaders != null) {
|
||||||
|
for (String contentIdHeader : contentIdHeaders) {
|
||||||
|
if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) {
|
||||||
|
attachment.setEmbedded(true);
|
||||||
|
String contentId = contentIdHeader.trim().replaceAll("[<>]", "");
|
||||||
|
attachment.setContentId(contentId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((request != null && request.isIncludeAttachments()) || attachment.isEmbedded()) {
|
||||||
|
extractAttachmentData(part, attachment, getContent, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
content.getAttachments().add(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void extractAttachmentData(
|
||||||
|
Object part, EmailAttachment attachment, Method getContent, EmlToPdfRequest request) {
|
||||||
|
try {
|
||||||
|
Object attachmentContent = getContent.invoke(part);
|
||||||
|
byte[] attachmentData = null;
|
||||||
|
|
||||||
|
if (attachmentContent instanceof InputStream inputStream) {
|
||||||
|
try (InputStream stream = inputStream) {
|
||||||
|
attachmentData = stream.readAllBytes();
|
||||||
|
} catch (IOException e) {
|
||||||
|
if (attachment.isEmbedded()) {
|
||||||
|
attachmentData = new byte[0];
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (attachmentContent instanceof byte[] byteArray) {
|
||||||
|
attachmentData = byteArray;
|
||||||
|
} else if (attachmentContent instanceof String stringContent) {
|
||||||
|
attachmentData = stringContent.getBytes(StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attachmentData != null) {
|
||||||
|
long maxSizeMB = request != null ? request.getMaxAttachmentSizeMB() : 10L;
|
||||||
|
long maxSizeBytes = maxSizeMB * 1024 * 1024;
|
||||||
|
|
||||||
|
if (attachmentData.length <= maxSizeBytes || attachment.isEmbedded()) {
|
||||||
|
attachment.setData(attachmentData);
|
||||||
|
attachment.setSizeBytes(attachmentData.length);
|
||||||
|
} else {
|
||||||
|
attachment.setSizeBytes(attachmentData.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||||
|
// Continue without attachment data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractBasicHeader(String emlContent, String headerName) {
|
||||||
|
try {
|
||||||
|
String[] lines = emlContent.split("\r?\n");
|
||||||
|
for (int i = 0; i < lines.length; i++) {
|
||||||
|
String line = lines[i];
|
||||||
|
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
||||||
|
StringBuilder value =
|
||||||
|
new StringBuilder(line.substring(headerName.length()).trim());
|
||||||
|
for (int j = i + 1; j < lines.length; j++) {
|
||||||
|
if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) {
|
||||||
|
value.append(" ").append(lines[j].trim());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return safeMimeDecode(value.toString());
|
||||||
|
}
|
||||||
|
if (line.trim().isEmpty()) break;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// Ignore errors in header extraction
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractHtmlBody(String emlContent) {
|
||||||
|
try {
|
||||||
|
String lowerContent = emlContent.toLowerCase();
|
||||||
|
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
|
||||||
|
if (htmlStart == -1) return null;
|
||||||
|
|
||||||
|
int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart);
|
||||||
|
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart);
|
||||||
|
if (bodyStart == -1) return null;
|
||||||
|
|
||||||
|
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
|
||||||
|
int bodyEnd = findPartEnd(emlContent, bodyStart);
|
||||||
|
|
||||||
|
return emlContent.substring(bodyStart, bodyEnd).trim();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractTextBody(String emlContent) {
|
||||||
|
try {
|
||||||
|
String lowerContent = emlContent.toLowerCase();
|
||||||
|
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
|
||||||
|
if (textStart == -1) {
|
||||||
|
int bodyStart = emlContent.indexOf("\r\n\r\n");
|
||||||
|
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n");
|
||||||
|
if (bodyStart != -1) {
|
||||||
|
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
|
||||||
|
int bodyEnd = findPartEnd(emlContent, bodyStart);
|
||||||
|
return emlContent.substring(bodyStart, bodyEnd).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bodyStart = emlContent.indexOf("\r\n\r\n", textStart);
|
||||||
|
if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", textStart);
|
||||||
|
if (bodyStart == -1) return null;
|
||||||
|
|
||||||
|
bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2;
|
||||||
|
int bodyEnd = findPartEnd(emlContent, bodyStart);
|
||||||
|
|
||||||
|
return emlContent.substring(bodyStart, bodyEnd).trim();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int findPartEnd(String content, int start) {
|
||||||
|
String[] lines = content.substring(start).split("\r?\n");
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.startsWith("--") && line.length() > 10) break;
|
||||||
|
result.append(line).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return start + result.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<EmailAttachment> extractAttachmentsBasic(String emlContent) {
|
||||||
|
List<EmailAttachment> attachments = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
String[] lines = emlContent.split("\r?\n");
|
||||||
|
boolean inHeaders = true;
|
||||||
|
String currentContentType = "";
|
||||||
|
String currentDisposition = "";
|
||||||
|
String currentFilename = "";
|
||||||
|
String currentEncoding = "";
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
String lowerLine = line.toLowerCase().trim();
|
||||||
|
|
||||||
|
if (line.trim().isEmpty()) {
|
||||||
|
inHeaders = false;
|
||||||
|
if (isAttachment(currentDisposition, currentFilename, currentContentType)) {
|
||||||
|
EmailAttachment attachment = new EmailAttachment();
|
||||||
|
attachment.setFilename(currentFilename);
|
||||||
|
attachment.setContentType(currentContentType);
|
||||||
|
attachment.setTransferEncoding(currentEncoding);
|
||||||
|
attachments.add(attachment);
|
||||||
|
}
|
||||||
|
currentContentType = "";
|
||||||
|
currentDisposition = "";
|
||||||
|
currentFilename = "";
|
||||||
|
currentEncoding = "";
|
||||||
|
inHeaders = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inHeaders) continue;
|
||||||
|
|
||||||
|
if (lowerLine.startsWith(HEADER_CONTENT_TYPE)) {
|
||||||
|
currentContentType = line.substring(HEADER_CONTENT_TYPE.length()).trim();
|
||||||
|
} else if (lowerLine.startsWith(HEADER_CONTENT_DISPOSITION)) {
|
||||||
|
currentDisposition = line.substring(HEADER_CONTENT_DISPOSITION.length()).trim();
|
||||||
|
currentFilename = extractFilenameFromDisposition(currentDisposition);
|
||||||
|
} else if (lowerLine.startsWith(HEADER_CONTENT_TRANSFER_ENCODING)) {
|
||||||
|
currentEncoding =
|
||||||
|
line.substring(HEADER_CONTENT_TRANSFER_ENCODING.length()).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
// Continue with empty list
|
||||||
|
}
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAttachment(String disposition, String filename, String contentType) {
|
||||||
|
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|
||||||
|
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|
||||||
|
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String extractFilenameFromDisposition(String disposition) {
|
||||||
|
if (disposition == null || !disposition.contains("filename=")) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle filename*= (RFC 2231 encoded filename)
|
||||||
|
if (disposition.toLowerCase().contains("filename*=")) {
|
||||||
|
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
|
||||||
|
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
|
||||||
|
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
||||||
|
String extendedFilename =
|
||||||
|
disposition.substring(filenameStarStart, filenameStarEnd).trim();
|
||||||
|
extendedFilename = extendedFilename.replaceAll("^\"|\"$", "");
|
||||||
|
|
||||||
|
if (extendedFilename.contains("'")) {
|
||||||
|
String[] parts = extendedFilename.split("'", 3);
|
||||||
|
if (parts.length == 3) {
|
||||||
|
return EmlProcessingUtils.decodeUrlEncoded(parts[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular filename=
|
||||||
|
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
|
||||||
|
int filenameEnd = disposition.indexOf(";", filenameStart);
|
||||||
|
if (filenameEnd == -1) filenameEnd = disposition.length();
|
||||||
|
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
||||||
|
filename = filename.replaceAll("^\"|\"$", "");
|
||||||
|
return safeMimeDecode(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String safeMimeDecode(String headerValue) {
|
||||||
|
if (headerValue == null || headerValue.trim().isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mimeUtilityChecked) {
|
||||||
|
synchronized (EmlParser.class) {
|
||||||
|
if (!mimeUtilityChecked) {
|
||||||
|
initializeMimeUtilityDecoding();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeUtilityDecodeTextMethod != null) {
|
||||||
|
try {
|
||||||
|
return (String) mimeUtilityDecodeTextMethod.invoke(null, headerValue.trim());
|
||||||
|
} catch (ReflectiveOperationException | RuntimeException e) {
|
||||||
|
// Fall through to custom implementation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmlProcessingUtils.decodeMimeHeader(headerValue.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initializeMimeUtilityDecoding() {
|
||||||
|
try {
|
||||||
|
Class<?> mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility");
|
||||||
|
mimeUtilityDecodeTextMethod = mimeUtilityClass.getMethod("decodeText", String.class);
|
||||||
|
} catch (ClassNotFoundException | NoSuchMethodException e) {
|
||||||
|
mimeUtilityDecodeTextMethod = null;
|
||||||
|
}
|
||||||
|
mimeUtilityChecked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class EmailContent {
|
||||||
|
private String subject;
|
||||||
|
private String from;
|
||||||
|
private String to;
|
||||||
|
private String cc;
|
||||||
|
private String bcc;
|
||||||
|
private Date date;
|
||||||
|
private String dateString; // For basic parsing fallback
|
||||||
|
private String htmlBody;
|
||||||
|
private String textBody;
|
||||||
|
private int attachmentCount;
|
||||||
|
private List<EmailAttachment> attachments = new ArrayList<>();
|
||||||
|
|
||||||
|
public void setHtmlBody(String htmlBody) {
|
||||||
|
this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTextBody(String textBody) {
|
||||||
|
this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class EmailAttachment {
|
||||||
|
private String filename;
|
||||||
|
private String contentType;
|
||||||
|
private byte[] data;
|
||||||
|
private boolean embedded;
|
||||||
|
private String embeddedFilename;
|
||||||
|
private long sizeBytes;
|
||||||
|
private String contentId;
|
||||||
|
private String disposition;
|
||||||
|
private String transferEncoding;
|
||||||
|
|
||||||
|
public void setData(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
if (data != null) {
|
||||||
|
this.sizeBytes = data.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,601 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class EmlProcessingUtils {
|
||||||
|
|
||||||
|
// Style constants
|
||||||
|
private static final int DEFAULT_FONT_SIZE = 12;
|
||||||
|
private static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif";
|
||||||
|
private static final float DEFAULT_LINE_HEIGHT = 1.4f;
|
||||||
|
private static final String DEFAULT_ZOOM = "1.0";
|
||||||
|
private static final String DEFAULT_TEXT_COLOR = "#202124";
|
||||||
|
private static final String DEFAULT_BACKGROUND_COLOR = "#ffffff";
|
||||||
|
private static final String DEFAULT_BORDER_COLOR = "#e8eaed";
|
||||||
|
private static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9";
|
||||||
|
private static final String ATTACHMENT_BORDER_COLOR = "#eeeeee";
|
||||||
|
|
||||||
|
private static final int EML_CHECK_LENGTH = 8192;
|
||||||
|
private static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2;
|
||||||
|
|
||||||
|
// MIME type detection
|
||||||
|
private static final Map<String, String> EXTENSION_TO_MIME_TYPE =
|
||||||
|
Map.of(
|
||||||
|
".png", "image/png",
|
||||||
|
".jpg", "image/jpeg",
|
||||||
|
".jpeg", "image/jpeg",
|
||||||
|
".gif", "image/gif",
|
||||||
|
".bmp", "image/bmp",
|
||||||
|
".webp", "image/webp",
|
||||||
|
".svg", "image/svg+xml",
|
||||||
|
".ico", "image/x-icon",
|
||||||
|
".tiff", "image/tiff",
|
||||||
|
".tif", "image/tiff");
|
||||||
|
|
||||||
|
public static void validateEmlInput(byte[] emlBytes) {
|
||||||
|
if (emlBytes == null || emlBytes.length == 0) {
|
||||||
|
throw new IllegalArgumentException("EML file is empty or null");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInvalidEmlFormat(emlBytes)) {
|
||||||
|
throw new IllegalArgumentException("Invalid EML file format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isInvalidEmlFormat(byte[] emlBytes) {
|
||||||
|
try {
|
||||||
|
int checkLength = Math.min(emlBytes.length, EML_CHECK_LENGTH);
|
||||||
|
String content;
|
||||||
|
|
||||||
|
try {
|
||||||
|
content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8);
|
||||||
|
if (content.contains("\uFFFD")) {
|
||||||
|
content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String lowerContent = content.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
boolean hasFrom =
|
||||||
|
lowerContent.contains("from:") || lowerContent.contains("return-path:");
|
||||||
|
boolean hasSubject = lowerContent.contains("subject:");
|
||||||
|
boolean hasMessageId = lowerContent.contains("message-id:");
|
||||||
|
boolean hasDate = lowerContent.contains("date:");
|
||||||
|
boolean hasTo =
|
||||||
|
lowerContent.contains("to:")
|
||||||
|
|| lowerContent.contains("cc:")
|
||||||
|
|| lowerContent.contains("bcc:");
|
||||||
|
boolean hasMimeStructure =
|
||||||
|
lowerContent.contains("multipart/")
|
||||||
|
|| lowerContent.contains("text/plain")
|
||||||
|
|| lowerContent.contains("text/html")
|
||||||
|
|| lowerContent.contains("boundary=");
|
||||||
|
|
||||||
|
int headerCount = 0;
|
||||||
|
if (hasFrom) headerCount++;
|
||||||
|
if (hasSubject) headerCount++;
|
||||||
|
if (hasMessageId) headerCount++;
|
||||||
|
if (hasDate) headerCount++;
|
||||||
|
if (hasTo) headerCount++;
|
||||||
|
|
||||||
|
return headerCount < MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure;
|
||||||
|
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateEnhancedEmailHtml(
|
||||||
|
EmlParser.EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
StringBuilder html = new StringBuilder();
|
||||||
|
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><meta charset="UTF-8">
|
||||||
|
<title>%s</title>
|
||||||
|
<style>
|
||||||
|
""",
|
||||||
|
sanitizeText(content.getSubject(), customHtmlSanitizer)));
|
||||||
|
|
||||||
|
appendEnhancedStyles(html);
|
||||||
|
|
||||||
|
html.append(
|
||||||
|
"""
|
||||||
|
</style>
|
||||||
|
</head><body>
|
||||||
|
""");
|
||||||
|
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"""
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="email-header">
|
||||||
|
<h1>%s</h1>
|
||||||
|
<div class="email-meta">
|
||||||
|
<div><strong>From:</strong> %s</div>
|
||||||
|
<div><strong>To:</strong> %s</div>
|
||||||
|
""",
|
||||||
|
sanitizeText(content.getSubject(), customHtmlSanitizer),
|
||||||
|
sanitizeText(content.getFrom(), customHtmlSanitizer),
|
||||||
|
sanitizeText(content.getTo(), customHtmlSanitizer)));
|
||||||
|
|
||||||
|
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"<div><strong>CC:</strong> %s</div>\n",
|
||||||
|
sanitizeText(content.getCc(), customHtmlSanitizer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"<div><strong>BCC:</strong> %s</div>\n",
|
||||||
|
sanitizeText(content.getBcc(), customHtmlSanitizer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.getDate() != null) {
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"<div><strong>Date:</strong> %s</div>\n",
|
||||||
|
PdfAttachmentHandler.formatEmailDate(content.getDate())));
|
||||||
|
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"<div><strong>Date:</strong> %s</div>\n",
|
||||||
|
sanitizeText(content.getDateString(), customHtmlSanitizer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append("</div></div>\n");
|
||||||
|
|
||||||
|
html.append("<div class=\"email-body\">\n");
|
||||||
|
if (content.getHtmlBody() != null && !content.getHtmlBody().trim().isEmpty()) {
|
||||||
|
String processedHtml =
|
||||||
|
processEmailHtmlBody(content.getHtmlBody(), content, customHtmlSanitizer);
|
||||||
|
html.append(processedHtml);
|
||||||
|
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"<div class=\"text-body\">%s</div>",
|
||||||
|
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
|
||||||
|
} else {
|
||||||
|
html.append("<div class=\"no-content\"><p><em>No content available</em></p></div>");
|
||||||
|
}
|
||||||
|
html.append("</div>\n");
|
||||||
|
|
||||||
|
if (content.getAttachmentCount() > 0 || !content.getAttachments().isEmpty()) {
|
||||||
|
appendAttachmentsSection(html, content, request, customHtmlSanitizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
html.append("</div>\n</body></html>");
|
||||||
|
return html.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String processEmailHtmlBody(
|
||||||
|
String htmlBody,
|
||||||
|
EmlParser.EmailContent emailContent,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
if (htmlBody == null) return "";
|
||||||
|
|
||||||
|
String processed =
|
||||||
|
customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody;
|
||||||
|
|
||||||
|
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", "");
|
||||||
|
processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", "");
|
||||||
|
|
||||||
|
if (emailContent != null && !emailContent.getAttachments().isEmpty()) {
|
||||||
|
processed = PdfAttachmentHandler.processInlineImages(processed, emailContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String convertTextToHtml(
|
||||||
|
String textBody, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
if (textBody == null) return "";
|
||||||
|
|
||||||
|
String html =
|
||||||
|
customHtmlSanitizer != null
|
||||||
|
? customHtmlSanitizer.sanitize(textBody)
|
||||||
|
: escapeHtml(textBody);
|
||||||
|
|
||||||
|
html = html.replace("\r\n", "\n").replace("\r", "\n");
|
||||||
|
html = html.replace("\n", "<br>\n");
|
||||||
|
|
||||||
|
html =
|
||||||
|
html.replaceAll(
|
||||||
|
"(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)",
|
||||||
|
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||||
|
|
||||||
|
html =
|
||||||
|
html.replaceAll(
|
||||||
|
"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})",
|
||||||
|
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendEnhancedStyles(StringBuilder html) {
|
||||||
|
String css =
|
||||||
|
String.format(
|
||||||
|
"""
|
||||||
|
body {
|
||||||
|
font-family: %s;
|
||||||
|
font-size: %dpx;
|
||||||
|
line-height: %s;
|
||||||
|
color: %s;
|
||||||
|
margin: 0;
|
||||||
|
padding: 16px;
|
||||||
|
background-color: %s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-container {
|
||||||
|
width: 100%%;
|
||||||
|
max-width: 100%%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 1px solid %s;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-header h1 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: %dpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-meta div {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: %dpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-body {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-section {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: %s;
|
||||||
|
border: 1px solid %s;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-section h3 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: %dpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-details, .attachment-type {
|
||||||
|
font-size: %dpx;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-inclusion-note, .attachment-info-note {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px;
|
||||||
|
font-size: %dpx;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-inclusion-note {
|
||||||
|
background-color: #e6ffed;
|
||||||
|
border: 1px solid #d4f7dc;
|
||||||
|
color: #006420;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-info-note {
|
||||||
|
background-color: #fff9e6;
|
||||||
|
border: 1px solid #fff0c2;
|
||||||
|
color: #664d00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-link-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-link-container:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-note {
|
||||||
|
font-size: %dpx;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-content {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
DEFAULT_FONT_FAMILY,
|
||||||
|
DEFAULT_FONT_SIZE,
|
||||||
|
DEFAULT_LINE_HEIGHT,
|
||||||
|
DEFAULT_TEXT_COLOR,
|
||||||
|
DEFAULT_BACKGROUND_COLOR,
|
||||||
|
DEFAULT_BORDER_COLOR,
|
||||||
|
DEFAULT_FONT_SIZE + 4,
|
||||||
|
DEFAULT_FONT_SIZE - 1,
|
||||||
|
ATTACHMENT_BACKGROUND_COLOR,
|
||||||
|
ATTACHMENT_BORDER_COLOR,
|
||||||
|
DEFAULT_FONT_SIZE + 1,
|
||||||
|
DEFAULT_FONT_SIZE - 2,
|
||||||
|
DEFAULT_FONT_SIZE - 2,
|
||||||
|
DEFAULT_FONT_SIZE - 3);
|
||||||
|
|
||||||
|
html.append(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void appendAttachmentsSection(
|
||||||
|
StringBuilder html,
|
||||||
|
EmlParser.EmailContent content,
|
||||||
|
EmlToPdfRequest request,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
html.append("<div class=\"attachment-section\">\n");
|
||||||
|
int displayedAttachmentCount =
|
||||||
|
content.getAttachmentCount() > 0
|
||||||
|
? content.getAttachmentCount()
|
||||||
|
: content.getAttachments().size();
|
||||||
|
html.append("<h3>Attachments (").append(displayedAttachmentCount).append(")</h3>\n");
|
||||||
|
|
||||||
|
if (!content.getAttachments().isEmpty()) {
|
||||||
|
for (int i = 0; i < content.getAttachments().size(); i++) {
|
||||||
|
EmlParser.EmailAttachment attachment = content.getAttachments().get(i);
|
||||||
|
|
||||||
|
String embeddedFilename =
|
||||||
|
attachment.getFilename() != null
|
||||||
|
? attachment.getFilename()
|
||||||
|
: ("attachment_" + i);
|
||||||
|
attachment.setEmbeddedFilename(embeddedFilename);
|
||||||
|
|
||||||
|
String sizeStr = GeneralUtils.formatBytes(attachment.getSizeBytes());
|
||||||
|
String contentType =
|
||||||
|
attachment.getContentType() != null
|
||||||
|
&& !attachment.getContentType().isEmpty()
|
||||||
|
? ", " + escapeHtml(attachment.getContentType())
|
||||||
|
: "";
|
||||||
|
|
||||||
|
String attachmentId = "attachment_" + i;
|
||||||
|
html.append(
|
||||||
|
String.format(
|
||||||
|
"""
|
||||||
|
<div class="attachment-item" id="%s">
|
||||||
|
<span class="attachment-icon" data-filename="%s">@</span>
|
||||||
|
<span class="attachment-name">%s</span>
|
||||||
|
<span class="attachment-details">(%s%s)</span>
|
||||||
|
</div>
|
||||||
|
""",
|
||||||
|
attachmentId,
|
||||||
|
escapeHtml(embeddedFilename),
|
||||||
|
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
|
||||||
|
sizeStr,
|
||||||
|
contentType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request != null && request.isIncludeAttachments()) {
|
||||||
|
html.append(
|
||||||
|
"""
|
||||||
|
<div class="attachment-info-note">
|
||||||
|
<p><em>Attachments are embedded in the file.</em></p>
|
||||||
|
</div>
|
||||||
|
""");
|
||||||
|
} else {
|
||||||
|
html.append(
|
||||||
|
"""
|
||||||
|
<div class="attachment-info-note">
|
||||||
|
<p><em>Attachment information displayed - files not included in PDF.</em></p>
|
||||||
|
</div>
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
html.append("</div>\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) {
|
||||||
|
HTMLToPdfRequest htmlRequest = new HTMLToPdfRequest();
|
||||||
|
|
||||||
|
if (request != null) {
|
||||||
|
htmlRequest.setFileInput(request.getFileInput());
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlRequest.setZoom(Float.parseFloat(DEFAULT_ZOOM));
|
||||||
|
return htmlRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String detectMimeType(String filename, String existingMimeType) {
|
||||||
|
if (existingMimeType != null && !existingMimeType.isEmpty()) {
|
||||||
|
return existingMimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename != null) {
|
||||||
|
String lowerFilename = filename.toLowerCase();
|
||||||
|
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
|
||||||
|
if (lowerFilename.endsWith(entry.getKey())) {
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "image/png";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decodeUrlEncoded(String encoded) {
|
||||||
|
try {
|
||||||
|
return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return encoded; // Return original if decoding fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String decodeMimeHeader(String encodedText) {
|
||||||
|
if (encodedText == null || encodedText.trim().isEmpty()) {
|
||||||
|
return encodedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
Pattern concatenatedPattern =
|
||||||
|
Pattern.compile(
|
||||||
|
"(=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)(\\s*=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)+");
|
||||||
|
Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText);
|
||||||
|
String processedText =
|
||||||
|
concatenatedMatcher.replaceAll(
|
||||||
|
match -> match.group().replaceAll("\\s+(?==\\?)", ""));
|
||||||
|
|
||||||
|
Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?=");
|
||||||
|
Matcher matcher = mimePattern.matcher(processedText);
|
||||||
|
int lastEnd = 0;
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
result.append(processedText, lastEnd, matcher.start());
|
||||||
|
|
||||||
|
String charset = matcher.group(1);
|
||||||
|
String encoding = matcher.group(2).toUpperCase();
|
||||||
|
String encodedValue = matcher.group(3);
|
||||||
|
|
||||||
|
try {
|
||||||
|
String decodedValue =
|
||||||
|
switch (encoding) {
|
||||||
|
case "B" -> {
|
||||||
|
String cleanBase64 = encodedValue.replaceAll("\\s", "");
|
||||||
|
byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64);
|
||||||
|
Charset targetCharset;
|
||||||
|
try {
|
||||||
|
targetCharset = Charset.forName(charset);
|
||||||
|
} catch (Exception e) {
|
||||||
|
targetCharset = StandardCharsets.UTF_8;
|
||||||
|
}
|
||||||
|
yield new String(decodedBytes, targetCharset);
|
||||||
|
}
|
||||||
|
case "Q" -> decodeQuotedPrintable(encodedValue, charset);
|
||||||
|
default -> matcher.group(0); // Return original if unknown encoding
|
||||||
|
};
|
||||||
|
result.append(decodedValue);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
result.append(matcher.group(0)); // Keep original on decode error
|
||||||
|
}
|
||||||
|
|
||||||
|
lastEnd = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
result.append(processedText.substring(lastEnd));
|
||||||
|
return result.toString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return encodedText; // Return original on any parsing error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String decodeQuotedPrintable(String encodedText, String charset) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (int i = 0; i < encodedText.length(); i++) {
|
||||||
|
char c = encodedText.charAt(i);
|
||||||
|
switch (c) {
|
||||||
|
case '=' -> {
|
||||||
|
if (i + 2 < encodedText.length()) {
|
||||||
|
String hex = encodedText.substring(i + 1, i + 3);
|
||||||
|
try {
|
||||||
|
int value = Integer.parseInt(hex, 16);
|
||||||
|
result.append((char) value);
|
||||||
|
i += 2;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
} else if (i + 1 == encodedText.length()
|
||||||
|
|| (i + 2 == encodedText.length()
|
||||||
|
&& encodedText.charAt(i + 1) == '\n')) {
|
||||||
|
if (i + 1 < encodedText.length() && encodedText.charAt(i + 1) == '\n') {
|
||||||
|
i++; // Skip the newline too
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case '_' -> result.append(' '); // Space encoding in Q encoding
|
||||||
|
default -> result.append(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1);
|
||||||
|
try {
|
||||||
|
Charset targetCharset = Charset.forName(charset);
|
||||||
|
return new String(bytes, targetCharset);
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception fallbackException) {
|
||||||
|
return new String(bytes, StandardCharsets.ISO_8859_1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String escapeHtml(String text) {
|
||||||
|
if (text == null) return "";
|
||||||
|
return text.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\"", """)
|
||||||
|
.replace("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
if (customHtmlSanitizer != null) {
|
||||||
|
return customHtmlSanitizer.sanitize(text);
|
||||||
|
} else {
|
||||||
|
return escapeHtml(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String simplifyHtmlContent(String htmlContent) {
|
||||||
|
String simplified = htmlContent.replaceAll("(?i)<script[^>]*>.*?</script>", "");
|
||||||
|
simplified = simplified.replaceAll("(?i)<style[^>]*>.*?</style>", "");
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -26,8 +26,8 @@ public class FileToPdf {
|
|||||||
HTMLToPdfRequest request,
|
HTMLToPdfRequest request,
|
||||||
byte[] fileBytes,
|
byte[] fileBytes,
|
||||||
String fileName,
|
String fileName,
|
||||||
boolean disableSanitize,
|
TempFileManager tempFileManager,
|
||||||
TempFileManager tempFileManager)
|
CustomHtmlSanitizer customHtmlSanitizer)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
||||||
@ -39,14 +39,15 @@ public class FileToPdf {
|
|||||||
if (fileName.toLowerCase().endsWith(".html")) {
|
if (fileName.toLowerCase().endsWith(".html")) {
|
||||||
String sanitizedHtml =
|
String sanitizedHtml =
|
||||||
sanitizeHtmlContent(
|
sanitizeHtmlContent(
|
||||||
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
new String(fileBytes, StandardCharsets.UTF_8),
|
||||||
|
customHtmlSanitizer);
|
||||||
Files.write(
|
Files.write(
|
||||||
tempInputFile.getPath(),
|
tempInputFile.getPath(),
|
||||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||||
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
||||||
Files.write(tempInputFile.getPath(), fileBytes);
|
Files.write(tempInputFile.getPath(), fileBytes);
|
||||||
sanitizeHtmlFilesInZip(
|
sanitizeHtmlFilesInZip(
|
||||||
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
|
||||||
} else {
|
} else {
|
||||||
throw ExceptionUtils.createHtmlFileRequiredException();
|
throw ExceptionUtils.createHtmlFileRequiredException();
|
||||||
}
|
}
|
||||||
@ -78,12 +79,15 @@ public class FileToPdf {
|
|||||||
} // tempOutputFile auto-closed
|
} // tempOutputFile auto-closed
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) {
|
private static String sanitizeHtmlContent(
|
||||||
return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent;
|
String htmlContent, CustomHtmlSanitizer customHtmlSanitizer) {
|
||||||
|
return customHtmlSanitizer.sanitize(htmlContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sanitizeHtmlFilesInZip(
|
private static void sanitizeHtmlFilesInZip(
|
||||||
Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager)
|
Path zipFilePath,
|
||||||
|
TempFileManager tempFileManager,
|
||||||
|
CustomHtmlSanitizer customHtmlSanitizer)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
|
try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
|
||||||
try (ZipInputStream zipIn =
|
try (ZipInputStream zipIn =
|
||||||
@ -99,7 +103,8 @@ public class FileToPdf {
|
|||||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
||||||
String content =
|
String content =
|
||||||
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
|
String sanitizedContent =
|
||||||
|
sanitizeHtmlContent(content, customHtmlSanitizer);
|
||||||
Files.write(
|
Files.write(
|
||||||
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
||||||
} else {
|
} else {
|
||||||
|
@ -14,6 +14,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
@ -34,8 +35,16 @@ import stirling.software.common.configuration.InstallationPathConfig;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class GeneralUtils {
|
public class GeneralUtils {
|
||||||
|
|
||||||
private static final List<String> DEFAULT_VALID_SCRIPTS =
|
private static final Set<String> DEFAULT_VALID_SCRIPTS =
|
||||||
List.of("png_to_webp.py", "split_photos.py");
|
Set.of("png_to_webp.py", "split_photos.py");
|
||||||
|
private static final Set<String> DEFAULT_VALID_PIPELINE =
|
||||||
|
Set.of(
|
||||||
|
"OCR images.json",
|
||||||
|
"Prepare-pdfs-for-email.json",
|
||||||
|
"split-rotate-auto-rename.json");
|
||||||
|
|
||||||
|
private static final String DEFAULT_WEBUI_CONFIGS_DIR = "defaultWebUIConfigs";
|
||||||
|
private static final String PYTHON_SCRIPTS_DIR = "python";
|
||||||
|
|
||||||
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
public static File convertMultipartFileToFile(MultipartFile multipartFile) throws IOException {
|
||||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||||
@ -447,7 +456,46 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts a file from classpath:/static/python to a temporary directory and returns the path.
|
* Extracts the default pipeline configurations from the classpath to the installation path.
|
||||||
|
* Creates directories if needed and copies default JSON files.
|
||||||
|
*
|
||||||
|
* <p>Existing files will be overwritten atomically (when supported). In case of unsupported
|
||||||
|
* atomic moves, falls back to non-atomic replace.
|
||||||
|
*
|
||||||
|
* @throws IOException if an I/O error occurs during file operations
|
||||||
|
*/
|
||||||
|
public static void extractPipeline() throws IOException {
|
||||||
|
Path pipelineDir =
|
||||||
|
Paths.get(InstallationPathConfig.getPipelinePath(), DEFAULT_WEBUI_CONFIGS_DIR);
|
||||||
|
Files.createDirectories(pipelineDir);
|
||||||
|
|
||||||
|
for (String name : DEFAULT_VALID_PIPELINE) {
|
||||||
|
if (!Paths.get(name).getFileName().toString().equals(name)) {
|
||||||
|
log.error("Invalid pipeline file name: {}", name);
|
||||||
|
throw new IllegalArgumentException("Invalid pipeline file name: " + name);
|
||||||
|
}
|
||||||
|
Path target = pipelineDir.resolve(name);
|
||||||
|
ClassPathResource res =
|
||||||
|
new ClassPathResource(
|
||||||
|
"static/pipeline/" + DEFAULT_WEBUI_CONFIGS_DIR + "/" + name);
|
||||||
|
if (!res.exists()) {
|
||||||
|
log.error("Resource not found: {}", res.getPath());
|
||||||
|
throw new IOException("Resource not found: " + res.getPath());
|
||||||
|
}
|
||||||
|
copyResourceToFile(res, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the specified Python script from the classpath to the installation path. Validates
|
||||||
|
* name and copies file atomically when possible, overwriting existing.
|
||||||
|
*
|
||||||
|
* <p>Existing files will be overwritten atomically (when supported).
|
||||||
|
*
|
||||||
|
* @param scriptName the name of the script to extract
|
||||||
|
* @return the path to the extracted script
|
||||||
|
* @throws IllegalArgumentException if the script name is invalid or not allowed
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public static Path extractScript(String scriptName) throws IOException {
|
public static Path extractScript(String scriptName) throws IOException {
|
||||||
// Validate input
|
// Validate input
|
||||||
@ -458,26 +506,71 @@ public class GeneralUtils {
|
|||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"scriptName must not contain path traversal characters");
|
"scriptName must not contain path traversal characters");
|
||||||
}
|
}
|
||||||
|
if (!Paths.get(scriptName).getFileName().toString().equals(scriptName)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"scriptName must not contain path traversal characters");
|
||||||
|
}
|
||||||
|
|
||||||
if (!DEFAULT_VALID_SCRIPTS.contains(scriptName)) {
|
if (!DEFAULT_VALID_SCRIPTS.contains(scriptName)) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"scriptName must be either 'png_to_webp.py' or 'split_photos.py'");
|
"scriptName must be either 'png_to_webp.py' or 'split_photos.py'");
|
||||||
}
|
}
|
||||||
|
|
||||||
Path scriptsDir = Paths.get(InstallationPathConfig.getScriptsPath(), "python");
|
Path scriptsDir = Paths.get(InstallationPathConfig.getScriptsPath(), PYTHON_SCRIPTS_DIR);
|
||||||
Files.createDirectories(scriptsDir);
|
Files.createDirectories(scriptsDir);
|
||||||
|
|
||||||
Path scriptFile = scriptsDir.resolve(scriptName);
|
Path target = scriptsDir.resolve(scriptName);
|
||||||
if (!Files.exists(scriptFile)) {
|
ClassPathResource res =
|
||||||
ClassPathResource resource = new ClassPathResource("static/python/" + scriptName);
|
new ClassPathResource("static/" + PYTHON_SCRIPTS_DIR + "/" + scriptName);
|
||||||
try (InputStream in = resource.getInputStream()) {
|
if (!res.exists()) {
|
||||||
Files.copy(in, scriptFile, StandardCopyOption.REPLACE_EXISTING);
|
log.error("Resource not found: {}", res.getPath());
|
||||||
|
throw new IOException("Resource not found: " + res.getPath());
|
||||||
|
}
|
||||||
|
copyResourceToFile(res, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a resource from the classpath to a specified target file.
|
||||||
|
*
|
||||||
|
* @param resource the ClassPathResource to copy
|
||||||
|
* @param target the target Path where the resource will be copied
|
||||||
|
* @throws IOException if an I/O error occurs during the copy operation
|
||||||
|
*/
|
||||||
|
private static void copyResourceToFile(ClassPathResource resource, Path target)
|
||||||
|
throws IOException {
|
||||||
|
Path dir = target.getParent();
|
||||||
|
Path tmp = Files.createTempFile(dir, target.getFileName().toString(), ".tmp");
|
||||||
|
try (InputStream in = resource.getInputStream()) {
|
||||||
|
Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
try {
|
||||||
|
Files.move(tmp, target, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch (AtomicMoveNotSupportedException e) {
|
||||||
|
log.warn(
|
||||||
|
"Atomic move not supported, falling back to non-atomic move for {}",
|
||||||
|
target,
|
||||||
|
e);
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} catch (FileAlreadyExistsException e) {
|
||||||
|
log.debug("File already exists at {}, attempting to replace it.", target);
|
||||||
|
Files.move(tmp, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch (AccessDeniedException e) {
|
||||||
|
log.error("Access denied while attempting to copy resource to {}", target, e);
|
||||||
|
throw e;
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
log.error("File system error occurred while copying resource to {}", target, e);
|
||||||
|
throw e;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to copy resource to {}", target, e);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(tmp);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Failed to extract Python script", e);
|
log.warn("Failed to delete temporary file {}", tmp, e);
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return scriptFile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||||
|
@ -5,8 +5,11 @@ import java.awt.image.*;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.imageio.ImageReader;
|
||||||
|
import javax.imageio.stream.ImageInputStream;
|
||||||
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@ -115,7 +118,36 @@ public class ImageProcessingUtils {
|
|||||||
|
|
||||||
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
|
public static BufferedImage loadImageWithExifOrientation(MultipartFile file)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
BufferedImage image = ImageIO.read(file.getInputStream());
|
BufferedImage image = null;
|
||||||
|
String filename = file.getOriginalFilename();
|
||||||
|
|
||||||
|
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
|
||||||
|
// For PSD files, try explicit ImageReader
|
||||||
|
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
|
||||||
|
if (readers.hasNext()) {
|
||||||
|
ImageReader reader = readers.next();
|
||||||
|
try (ImageInputStream iis = ImageIO.createImageInputStream(file.getInputStream())) {
|
||||||
|
reader.setInput(iis);
|
||||||
|
image = reader.read(0);
|
||||||
|
} finally {
|
||||||
|
reader.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (image == null) {
|
||||||
|
throw new IOException(
|
||||||
|
"Unable to read image from file: "
|
||||||
|
+ filename
|
||||||
|
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-PSD files, use standard ImageIO
|
||||||
|
image = ImageIO.read(file.getInputStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image == null) {
|
||||||
|
throw new IOException("Unable to read image from file: " + filename);
|
||||||
|
}
|
||||||
|
|
||||||
double orientation = extractImageOrientation(file.getInputStream());
|
double orientation = extractImageOrientation(file.getInputStream());
|
||||||
return applyOrientation(image, orientation);
|
return applyOrientation(image, orientation);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,680 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PageMode;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
||||||
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class PdfAttachmentHandler {
|
||||||
|
// Note: This class is designed for EML attachments, not general PDF attachments.
|
||||||
|
|
||||||
|
private static final String ATTACHMENT_MARKER = "@";
|
||||||
|
private static final float ATTACHMENT_ICON_WIDTH = 12f;
|
||||||
|
private static final float ATTACHMENT_ICON_HEIGHT = 14f;
|
||||||
|
private static final float ANNOTATION_X_OFFSET = 2f;
|
||||||
|
private static final float ANNOTATION_Y_OFFSET = 10f;
|
||||||
|
|
||||||
|
public static byte[] attachFilesToPdf(
|
||||||
|
byte[] pdfBytes,
|
||||||
|
List<EmlParser.EmailAttachment> attachments,
|
||||||
|
CustomPDFDocumentFactory pdfDocumentFactory)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
if (attachments == null || attachments.isEmpty()) {
|
||||||
|
return pdfBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
|
|
||||||
|
List<MultipartFile> multipartAttachments = new ArrayList<>(attachments.size());
|
||||||
|
for (int i = 0; i < attachments.size(); i++) {
|
||||||
|
EmlParser.EmailAttachment attachment = attachments.get(i);
|
||||||
|
if (attachment.getData() != null && attachment.getData().length > 0) {
|
||||||
|
String embeddedFilename =
|
||||||
|
attachment.getFilename() != null
|
||||||
|
? attachment.getFilename()
|
||||||
|
: ("attachment_" + i);
|
||||||
|
attachment.setEmbeddedFilename(embeddedFilename);
|
||||||
|
multipartAttachments.add(createMultipartFile(attachment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!multipartAttachments.isEmpty()) {
|
||||||
|
Map<Integer, String> indexToFilenameMap =
|
||||||
|
addAttachmentsToDocumentWithMapping(
|
||||||
|
document, multipartAttachments, attachments);
|
||||||
|
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
|
||||||
|
addAttachmentAnnotationsToDocumentWithMapping(
|
||||||
|
document, attachments, indexToFilenameMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.save(outputStream);
|
||||||
|
return outputStream.toByteArray();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw new IOException(
|
||||||
|
"Invalid PDF structure or processing error: " + e.getMessage(), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IOException("Error attaching files to PDF: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MultipartFile createMultipartFile(EmlParser.EmailAttachment attachment) {
|
||||||
|
return new MultipartFile() {
|
||||||
|
@Override
|
||||||
|
public @NotNull String getName() {
|
||||||
|
return "attachment";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOriginalFilename() {
|
||||||
|
return attachment.getFilename() != null
|
||||||
|
? attachment.getFilename()
|
||||||
|
: "attachment_" + System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContentType() {
|
||||||
|
return attachment.getContentType() != null
|
||||||
|
? attachment.getContentType()
|
||||||
|
: "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return attachment.getData() == null || attachment.getData().length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getSize() {
|
||||||
|
return attachment.getData() != null ? attachment.getData().length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte @NotNull [] getBytes() {
|
||||||
|
return attachment.getData() != null ? attachment.getData() : new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull InputStream getInputStream() {
|
||||||
|
byte[] data = attachment.getData();
|
||||||
|
return new ByteArrayInputStream(data != null ? data : new byte[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transferTo(@NotNull File dest) throws IOException, IllegalStateException {
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(dest)) {
|
||||||
|
byte[] data = attachment.getData();
|
||||||
|
if (data != null) {
|
||||||
|
fos.write(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ensureUniqueFilename(String filename, Set<String> existingNames) {
|
||||||
|
if (!existingNames.contains(filename)) {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseName;
|
||||||
|
String extension = "";
|
||||||
|
int lastDot = filename.lastIndexOf('.');
|
||||||
|
if (lastDot > 0) {
|
||||||
|
baseName = filename.substring(0, lastDot);
|
||||||
|
extension = filename.substring(lastDot);
|
||||||
|
} else {
|
||||||
|
baseName = filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
int counter = 1;
|
||||||
|
String uniqueName;
|
||||||
|
do {
|
||||||
|
uniqueName = baseName + "_" + counter + extension;
|
||||||
|
counter++;
|
||||||
|
} while (existingNames.contains(uniqueName));
|
||||||
|
|
||||||
|
return uniqueName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static @NotNull PDRectangle calculateAnnotationRectangle(
|
||||||
|
PDPage page, float x, float y) {
|
||||||
|
PDRectangle cropBox = page.getCropBox();
|
||||||
|
|
||||||
|
// ISO 32000-1:2008 Section 8.3: PDF coordinate system transforms
|
||||||
|
int rotation = page.getRotation();
|
||||||
|
float pdfX = x;
|
||||||
|
float pdfY = cropBox.getHeight() - y;
|
||||||
|
|
||||||
|
switch (rotation) {
|
||||||
|
case 90 -> {
|
||||||
|
float temp = pdfX;
|
||||||
|
pdfX = pdfY;
|
||||||
|
pdfY = cropBox.getWidth() - temp;
|
||||||
|
}
|
||||||
|
case 180 -> {
|
||||||
|
pdfX = cropBox.getWidth() - pdfX;
|
||||||
|
pdfY = y;
|
||||||
|
}
|
||||||
|
case 270 -> {
|
||||||
|
float temp = pdfX;
|
||||||
|
pdfX = cropBox.getHeight() - pdfY;
|
||||||
|
pdfY = temp;
|
||||||
|
}
|
||||||
|
default -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
float iconHeight = ATTACHMENT_ICON_HEIGHT;
|
||||||
|
float paddingX = 2.0f;
|
||||||
|
float paddingY = 2.0f;
|
||||||
|
|
||||||
|
PDRectangle rect =
|
||||||
|
new PDRectangle(
|
||||||
|
pdfX + ANNOTATION_X_OFFSET + paddingX,
|
||||||
|
pdfY - iconHeight + ANNOTATION_Y_OFFSET + paddingY,
|
||||||
|
ATTACHMENT_ICON_WIDTH,
|
||||||
|
iconHeight);
|
||||||
|
|
||||||
|
PDRectangle mediaBox = page.getMediaBox();
|
||||||
|
if (rect.getLowerLeftX() < mediaBox.getLowerLeftX()
|
||||||
|
|| rect.getLowerLeftY() < mediaBox.getLowerLeftY()
|
||||||
|
|| rect.getUpperRightX() > mediaBox.getUpperRightX()
|
||||||
|
|| rect.getUpperRightY() > mediaBox.getUpperRightY()) {
|
||||||
|
|
||||||
|
float adjustedX =
|
||||||
|
Math.max(
|
||||||
|
mediaBox.getLowerLeftX(),
|
||||||
|
Math.min(
|
||||||
|
rect.getLowerLeftX(),
|
||||||
|
mediaBox.getUpperRightX() - rect.getWidth()));
|
||||||
|
float adjustedY =
|
||||||
|
Math.max(
|
||||||
|
mediaBox.getLowerLeftY(),
|
||||||
|
Math.min(
|
||||||
|
rect.getLowerLeftY(),
|
||||||
|
mediaBox.getUpperRightY() - rect.getHeight()));
|
||||||
|
rect = new PDRectangle(adjustedX, adjustedY, rect.getWidth(), rect.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String processInlineImages(
|
||||||
|
String htmlContent, EmlParser.EmailContent emailContent) {
|
||||||
|
if (htmlContent == null || emailContent == null) return htmlContent;
|
||||||
|
|
||||||
|
Map<String, EmlParser.EmailAttachment> contentIdMap = new HashMap<>();
|
||||||
|
for (EmlParser.EmailAttachment attachment : emailContent.getAttachments()) {
|
||||||
|
if (attachment.isEmbedded()
|
||||||
|
&& attachment.getContentId() != null
|
||||||
|
&& attachment.getData() != null) {
|
||||||
|
contentIdMap.put(attachment.getContentId(), attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentIdMap.isEmpty()) return htmlContent;
|
||||||
|
|
||||||
|
Pattern cidPattern =
|
||||||
|
Pattern.compile(
|
||||||
|
"(?i)<img[^>]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
Matcher matcher = cidPattern.matcher(htmlContent);
|
||||||
|
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
while (matcher.find()) {
|
||||||
|
String contentId = matcher.group(1);
|
||||||
|
EmlParser.EmailAttachment attachment = contentIdMap.get(contentId);
|
||||||
|
|
||||||
|
if (attachment != null && attachment.getData() != null) {
|
||||||
|
String mimeType =
|
||||||
|
EmlProcessingUtils.detectMimeType(
|
||||||
|
attachment.getFilename(), attachment.getContentType());
|
||||||
|
|
||||||
|
String base64Data = Base64.getEncoder().encodeToString(attachment.getData());
|
||||||
|
String dataUri = "data:" + mimeType + ";base64," + base64Data;
|
||||||
|
|
||||||
|
String replacement =
|
||||||
|
matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri);
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||||
|
} else {
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatEmailDate(Date date) {
|
||||||
|
if (date == null) return "";
|
||||||
|
|
||||||
|
SimpleDateFormat formatter =
|
||||||
|
new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH);
|
||||||
|
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
return formatter.format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class MarkerPosition {
|
||||||
|
private int pageIndex;
|
||||||
|
private float x;
|
||||||
|
private float y;
|
||||||
|
private String character;
|
||||||
|
private String filename;
|
||||||
|
|
||||||
|
public MarkerPosition(int pageIndex, float x, float y, String character, String filename) {
|
||||||
|
this.pageIndex = pageIndex;
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.character = character;
|
||||||
|
this.filename = filename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AttachmentMarkerPositionFinder extends PDFTextStripper {
|
||||||
|
@Getter private final List<MarkerPosition> positions = new ArrayList<>();
|
||||||
|
private int currentPageIndex;
|
||||||
|
protected boolean sortByPosition;
|
||||||
|
private boolean isInAttachmentSection;
|
||||||
|
private boolean attachmentSectionFound;
|
||||||
|
private final StringBuilder currentText = new StringBuilder();
|
||||||
|
|
||||||
|
private static final Pattern ATTACHMENT_SECTION_PATTERN =
|
||||||
|
Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Pattern FILENAME_PATTERN =
|
||||||
|
Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)");
|
||||||
|
|
||||||
|
public AttachmentMarkerPositionFinder() {
|
||||||
|
super();
|
||||||
|
this.currentPageIndex = 0;
|
||||||
|
this.sortByPosition = false; // Disable sorting to preserve document order
|
||||||
|
this.isInAttachmentSection = false;
|
||||||
|
this.attachmentSectionFound = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getText(PDDocument document) throws IOException {
|
||||||
|
super.getText(document);
|
||||||
|
|
||||||
|
if (sortByPosition) {
|
||||||
|
positions.sort(
|
||||||
|
(a, b) -> {
|
||||||
|
int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex());
|
||||||
|
if (pageCompare != 0) return pageCompare;
|
||||||
|
return Float.compare(
|
||||||
|
b.getY(), a.getY()); // Descending Y per PDF coordinate system
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""; // Return empty string as we only need positions
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void startPage(PDPage page) throws IOException {
|
||||||
|
super.startPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void endPage(PDPage page) throws IOException {
|
||||||
|
currentPageIndex++;
|
||||||
|
super.endPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void writeString(String string, List<TextPosition> textPositions)
|
||||||
|
throws IOException {
|
||||||
|
String lowerString = string.toLowerCase();
|
||||||
|
|
||||||
|
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||||
|
isInAttachmentSection = true;
|
||||||
|
attachmentSectionFound = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInAttachmentSection
|
||||||
|
&& (lowerString.contains("</body>")
|
||||||
|
|| lowerString.contains("</html>")
|
||||||
|
|| (attachmentSectionFound
|
||||||
|
&& lowerString.trim().isEmpty()
|
||||||
|
&& string.length() > 50))) {
|
||||||
|
isInAttachmentSection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInAttachmentSection) {
|
||||||
|
currentText.append(string);
|
||||||
|
|
||||||
|
for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) {
|
||||||
|
if (i < textPositions.size()) {
|
||||||
|
TextPosition textPosition = textPositions.get(i);
|
||||||
|
|
||||||
|
String filename = extractFilenameAfterMarker(string, i);
|
||||||
|
|
||||||
|
MarkerPosition position =
|
||||||
|
new MarkerPosition(
|
||||||
|
currentPageIndex,
|
||||||
|
textPosition.getXDirAdj(),
|
||||||
|
textPosition.getYDirAdj(),
|
||||||
|
ATTACHMENT_MARKER,
|
||||||
|
filename);
|
||||||
|
positions.add(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.writeString(string, textPositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSortByPosition(boolean sortByPosition) {
|
||||||
|
this.sortByPosition = sortByPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractFilenameAfterMarker(String text, int markerIndex) {
|
||||||
|
String afterMarker = text.substring(markerIndex + 1);
|
||||||
|
|
||||||
|
Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker);
|
||||||
|
if (matcher.find()) {
|
||||||
|
return matcher.group(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] parts = afterMarker.split("[\\s\\(\\)]+");
|
||||||
|
for (String part : parts) {
|
||||||
|
part = part.trim();
|
||||||
|
if (part.length() > 3 && part.contains(".")) {
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Integer, String> addAttachmentsToDocumentWithMapping(
|
||||||
|
PDDocument document,
|
||||||
|
List<MultipartFile> attachments,
|
||||||
|
List<EmlParser.EmailAttachment> originalAttachments)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
|
||||||
|
if (catalog == null) {
|
||||||
|
throw new IOException("PDF document catalog is not accessible");
|
||||||
|
}
|
||||||
|
|
||||||
|
PDDocumentNameDictionary documentNames = catalog.getNames();
|
||||||
|
if (documentNames == null) {
|
||||||
|
documentNames = new PDDocumentNameDictionary(catalog);
|
||||||
|
catalog.setNames(documentNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles();
|
||||||
|
if (embeddedFilesTree == null) {
|
||||||
|
embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
|
||||||
|
documentNames.setEmbeddedFiles(embeddedFilesTree);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, PDComplexFileSpecification> existingNames = embeddedFilesTree.getNames();
|
||||||
|
if (existingNames == null) {
|
||||||
|
existingNames = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, String> indexToFilenameMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < attachments.size(); i++) {
|
||||||
|
MultipartFile attachment = attachments.get(i);
|
||||||
|
String filename = attachment.getOriginalFilename();
|
||||||
|
if (filename == null || filename.trim().isEmpty()) {
|
||||||
|
filename = "attachment_" + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedFilename =
|
||||||
|
isAscii(filename)
|
||||||
|
? filename
|
||||||
|
: java.text.Normalizer.normalize(
|
||||||
|
filename, java.text.Normalizer.Form.NFC);
|
||||||
|
String uniqueFilename =
|
||||||
|
ensureUniqueFilename(normalizedFilename, existingNames.keySet());
|
||||||
|
|
||||||
|
indexToFilenameMap.put(i, uniqueFilename);
|
||||||
|
|
||||||
|
PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream());
|
||||||
|
embeddedFile.setSize((int) attachment.getSize());
|
||||||
|
|
||||||
|
GregorianCalendar currentTime = new GregorianCalendar();
|
||||||
|
embeddedFile.setCreationDate(currentTime);
|
||||||
|
embeddedFile.setModDate(currentTime);
|
||||||
|
|
||||||
|
String contentType = attachment.getContentType();
|
||||||
|
if (contentType != null && !contentType.trim().isEmpty()) {
|
||||||
|
embeddedFile.setSubtype(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification();
|
||||||
|
fileSpecification.setFile(uniqueFilename);
|
||||||
|
fileSpecification.setFileUnicode(uniqueFilename);
|
||||||
|
fileSpecification.setEmbeddedFile(embeddedFile);
|
||||||
|
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
|
||||||
|
|
||||||
|
existingNames.put(uniqueFilename, fileSpecification);
|
||||||
|
}
|
||||||
|
|
||||||
|
embeddedFilesTree.setNames(existingNames);
|
||||||
|
documentNames.setEmbeddedFiles(embeddedFilesTree);
|
||||||
|
catalog.setNames(documentNames);
|
||||||
|
|
||||||
|
return indexToFilenameMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addAttachmentAnnotationsToDocumentWithMapping(
|
||||||
|
PDDocument document,
|
||||||
|
List<EmlParser.EmailAttachment> attachments,
|
||||||
|
Map<Integer, String> indexToFilenameMap)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder();
|
||||||
|
finder.setSortByPosition(false); // Keep document order to maintain pairing
|
||||||
|
finder.getText(document);
|
||||||
|
List<MarkerPosition> markerPositions = finder.getPositions();
|
||||||
|
|
||||||
|
int annotationsToAdd = Math.min(markerPositions.size(), attachments.size());
|
||||||
|
|
||||||
|
for (int i = 0; i < annotationsToAdd; i++) {
|
||||||
|
MarkerPosition position = markerPositions.get(i);
|
||||||
|
|
||||||
|
String filenameNearMarker = position.getFilename();
|
||||||
|
|
||||||
|
EmlParser.EmailAttachment matchingAttachment =
|
||||||
|
findAttachmentByFilename(attachments, filenameNearMarker);
|
||||||
|
|
||||||
|
if (matchingAttachment != null) {
|
||||||
|
String embeddedFilename =
|
||||||
|
findEmbeddedFilenameForAttachment(matchingAttachment, indexToFilenameMap);
|
||||||
|
|
||||||
|
if (embeddedFilename != null) {
|
||||||
|
PDPage page = document.getPage(position.getPageIndex());
|
||||||
|
addAttachmentAnnotationToPageWithMapping(
|
||||||
|
document,
|
||||||
|
page,
|
||||||
|
matchingAttachment,
|
||||||
|
embeddedFilename,
|
||||||
|
position.getX(),
|
||||||
|
position.getY(),
|
||||||
|
i);
|
||||||
|
} else {
|
||||||
|
// No embedded filename found for attachment
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No matching attachment found for filename near marker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmlParser.EmailAttachment findAttachmentByFilename(
|
||||||
|
List<EmlParser.EmailAttachment> attachments, String targetFilename) {
|
||||||
|
if (targetFilename == null || targetFilename.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedTarget = normalizeFilename(targetFilename);
|
||||||
|
|
||||||
|
// First try exact match
|
||||||
|
for (EmlParser.EmailAttachment attachment : attachments) {
|
||||||
|
if (attachment.getFilename() != null) {
|
||||||
|
String normalizedAttachment = normalizeFilename(attachment.getFilename());
|
||||||
|
if (normalizedAttachment.equals(normalizedTarget)) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then try contains match
|
||||||
|
for (EmlParser.EmailAttachment attachment : attachments) {
|
||||||
|
if (attachment.getFilename() != null) {
|
||||||
|
String normalizedAttachment = normalizeFilename(attachment.getFilename());
|
||||||
|
if (normalizedAttachment.contains(normalizedTarget)
|
||||||
|
|| normalizedTarget.contains(normalizedAttachment)) {
|
||||||
|
return attachment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String findEmbeddedFilenameForAttachment(
|
||||||
|
EmlParser.EmailAttachment attachment, Map<Integer, String> indexToFilenameMap) {
|
||||||
|
|
||||||
|
String attachmentFilename = attachment.getFilename();
|
||||||
|
if (attachmentFilename == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Integer, String> entry : indexToFilenameMap.entrySet()) {
|
||||||
|
String embeddedFilename = entry.getValue();
|
||||||
|
if (embeddedFilename != null
|
||||||
|
&& (embeddedFilename.equals(attachmentFilename)
|
||||||
|
|| embeddedFilename.contains(attachmentFilename)
|
||||||
|
|| attachmentFilename.contains(embeddedFilename))) {
|
||||||
|
return embeddedFilename;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeFilename(String filename) {
|
||||||
|
if (filename == null) return "";
|
||||||
|
return filename.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replaceAll("\\s+", " ")
|
||||||
|
.replaceAll("[^a-zA-Z0-9._-]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addAttachmentAnnotationToPageWithMapping(
|
||||||
|
PDDocument document,
|
||||||
|
PDPage page,
|
||||||
|
EmlParser.EmailAttachment attachment,
|
||||||
|
String embeddedFilename,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
int attachmentIndex)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment();
|
||||||
|
|
||||||
|
PDRectangle rect = calculateAnnotationRectangle(page, x, y);
|
||||||
|
fileAnnotation.setRectangle(rect);
|
||||||
|
|
||||||
|
fileAnnotation.setPrinted(false);
|
||||||
|
fileAnnotation.setHidden(false);
|
||||||
|
fileAnnotation.setNoView(false);
|
||||||
|
fileAnnotation.setNoZoom(true);
|
||||||
|
fileAnnotation.setNoRotate(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
|
||||||
|
PDAppearanceStream normalAppearance = new PDAppearanceStream(document);
|
||||||
|
normalAppearance.setBBox(new PDRectangle(0, 0, rect.getWidth(), rect.getHeight()));
|
||||||
|
appearance.setNormalAppearance(normalAppearance);
|
||||||
|
fileAnnotation.setAppearance(appearance);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
fileAnnotation.setAppearance(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
PDEmbeddedFilesNameTreeNode efTree =
|
||||||
|
document.getDocumentCatalog().getNames().getEmbeddedFiles();
|
||||||
|
if (efTree != null) {
|
||||||
|
Map<String, PDComplexFileSpecification> efMap = efTree.getNames();
|
||||||
|
if (efMap != null) {
|
||||||
|
PDComplexFileSpecification fileSpec = efMap.get(embeddedFilename);
|
||||||
|
if (fileSpec != null) {
|
||||||
|
fileAnnotation.setFile(fileSpec);
|
||||||
|
} else {
|
||||||
|
// Could not find embedded file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileAnnotation.setContents(
|
||||||
|
"Attachment " + (attachmentIndex + 1) + ": " + attachment.getFilename());
|
||||||
|
fileAnnotation.setAnnotationName(
|
||||||
|
"EmbeddedFile_" + attachmentIndex + "_" + embeddedFilename);
|
||||||
|
|
||||||
|
page.getAnnotations().add(fileAnnotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAscii(String str) {
|
||||||
|
if (str == null) return true;
|
||||||
|
for (int i = 0; i < str.length(); i++) {
|
||||||
|
if (str.charAt(i) > 127) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -145,13 +146,18 @@ public class PdfUtils {
|
|||||||
throws IOException, Exception {
|
throws IOException, Exception {
|
||||||
|
|
||||||
// Validate and limit DPI to prevent excessive memory usage
|
// Validate and limit DPI to prevent excessive memory usage
|
||||||
final int MAX_SAFE_DPI = 500; // Maximum safe DPI to prevent memory issues
|
int maxSafeDpi = 500; // Default maximum safe DPI
|
||||||
if (DPI > MAX_SAFE_DPI) {
|
ApplicationProperties properties =
|
||||||
|
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||||
|
if (properties != null && properties.getSystem() != null) {
|
||||||
|
maxSafeDpi = properties.getSystem().getMaxDPI();
|
||||||
|
}
|
||||||
|
if (DPI > maxSafeDpi) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.dpiExceedsLimit",
|
"error.dpiExceedsLimit",
|
||||||
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
|
"DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.",
|
||||||
DPI,
|
DPI,
|
||||||
MAX_SAFE_DPI);
|
maxSafeDpi);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
|
||||||
|
@ -7,24 +7,19 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.InjectMocks;
|
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -45,62 +40,44 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
private AutoJobAspect autoJobAspect;
|
private AutoJobAspect autoJobAspect;
|
||||||
|
|
||||||
@Mock
|
@Mock private JobExecutorService jobExecutorService;
|
||||||
private JobExecutorService jobExecutorService;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private HttpServletRequest request;
|
||||||
private HttpServletRequest request;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private FileOrUploadService fileOrUploadService;
|
||||||
private FileOrUploadService fileOrUploadService;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private FileStorage fileStorage;
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
|
@Mock private ResourceMonitor resourceMonitor;
|
||||||
|
|
||||||
@Mock
|
@Mock private JobQueue jobQueue;
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
autoJobAspect = new AutoJobAspect(
|
autoJobAspect =
|
||||||
jobExecutorService,
|
new AutoJobAspect(jobExecutorService, request, fileOrUploadService, fileStorage);
|
||||||
request,
|
|
||||||
fileOrUploadService,
|
|
||||||
fileStorage
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mock
|
@Mock private ProceedingJoinPoint joinPoint;
|
||||||
private ProceedingJoinPoint joinPoint;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private AutoJobPostMapping autoJobPostMapping;
|
||||||
private AutoJobPostMapping autoJobPostMapping;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<Supplier<Object>> workCaptor;
|
||||||
private ArgumentCaptor<Supplier<Object>> workCaptor;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<Boolean> asyncCaptor;
|
||||||
private ArgumentCaptor<Boolean> asyncCaptor;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<Long> timeoutCaptor;
|
||||||
private ArgumentCaptor<Long> timeoutCaptor;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<Boolean> queueableCaptor;
|
||||||
private ArgumentCaptor<Boolean> queueableCaptor;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<Integer> resourceWeightCaptor;
|
||||||
private ArgumentCaptor<Integer> resourceWeightCaptor;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExecuteWithCustomParameters() throws Throwable {
|
void shouldExecuteWithCustomParameters() throws Throwable {
|
||||||
// Given
|
// Given
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileId("test-file-id");
|
pdfFile.setFileId("test-file-id");
|
||||||
Object[] args = new Object[] { pdfFile };
|
Object[] args = new Object[] {pdfFile};
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
@ -113,9 +90,8 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
MultipartFile mockFile = mock(MultipartFile.class);
|
MultipartFile mockFile = mock(MultipartFile.class);
|
||||||
when(fileStorage.retrieveFile("test-file-id")).thenReturn(mockFile);
|
when(fileStorage.retrieveFile("test-file-id")).thenReturn(mockFile);
|
||||||
|
|
||||||
|
|
||||||
when(jobExecutorService.runJobGeneric(
|
when(jobExecutorService.runJobGeneric(
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
.thenReturn(ResponseEntity.ok("success"));
|
.thenReturn(ResponseEntity.ok("success"));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
@ -124,12 +100,13 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
// Then
|
// Then
|
||||||
assertEquals(ResponseEntity.ok("success"), result);
|
assertEquals(ResponseEntity.ok("success"), result);
|
||||||
|
|
||||||
verify(jobExecutorService).runJobGeneric(
|
verify(jobExecutorService)
|
||||||
asyncCaptor.capture(),
|
.runJobGeneric(
|
||||||
workCaptor.capture(),
|
asyncCaptor.capture(),
|
||||||
timeoutCaptor.capture(),
|
workCaptor.capture(),
|
||||||
queueableCaptor.capture(),
|
timeoutCaptor.capture(),
|
||||||
resourceWeightCaptor.capture());
|
queueableCaptor.capture(),
|
||||||
|
resourceWeightCaptor.capture());
|
||||||
|
|
||||||
assertTrue(asyncCaptor.getValue(), "Async should be true");
|
assertTrue(asyncCaptor.getValue(), "Async should be true");
|
||||||
assertEquals(60000L, timeoutCaptor.getValue(), "Timeout should be 60000ms");
|
assertEquals(60000L, timeoutCaptor.getValue(), "Timeout should be 60000ms");
|
||||||
@ -158,11 +135,12 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
// Mock jobExecutorService to execute the work immediately
|
// Mock jobExecutorService to execute the work immediately
|
||||||
when(jobExecutorService.runJobGeneric(
|
when(jobExecutorService.runJobGeneric(
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(
|
||||||
Supplier<Object> work = invocation.getArgument(1);
|
invocation -> {
|
||||||
return work.get();
|
Supplier<Object> work = invocation.getArgument(1);
|
||||||
});
|
return work.get();
|
||||||
|
});
|
||||||
|
|
||||||
// When
|
// When
|
||||||
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
Object result = autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||||
@ -179,7 +157,7 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
// Given
|
// Given
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileInput(mock(MultipartFile.class));
|
pdfFile.setFileInput(mock(MultipartFile.class));
|
||||||
Object[] args = new Object[] { pdfFile };
|
Object[] args = new Object[] {pdfFile};
|
||||||
|
|
||||||
when(joinPoint.getArgs()).thenReturn(args);
|
when(joinPoint.getArgs()).thenReturn(args);
|
||||||
when(request.getParameter("async")).thenReturn("true");
|
when(request.getParameter("async")).thenReturn("true");
|
||||||
@ -190,14 +168,16 @@ class AutoJobPostMappingIntegrationTest {
|
|||||||
|
|
||||||
// Mock job executor to return a successful response
|
// Mock job executor to return a successful response
|
||||||
when(jobExecutorService.runJobGeneric(
|
when(jobExecutorService.runJobGeneric(
|
||||||
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
anyBoolean(), any(Supplier.class), anyLong(), anyBoolean(), anyInt()))
|
||||||
.thenReturn(ResponseEntity.ok("success"));
|
.thenReturn(ResponseEntity.ok("success"));
|
||||||
|
|
||||||
// When
|
// When
|
||||||
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
autoJobAspect.wrapWithJobExecution(joinPoint, autoJobPostMapping);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals("stored-file-id", pdfFile.getFileId(),
|
assertEquals(
|
||||||
|
"stored-file-id",
|
||||||
|
pdfFile.getFileId(),
|
||||||
"FileId should be set to the stored file id");
|
"FileId should be set to the stored file id");
|
||||||
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
assertNotNull(pdfFile.getFileInput(), "FileInput should be replaced with persistent file");
|
||||||
|
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.StandardEnvironment;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
|
class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loads_yaml_into_environment() throws Exception {
|
||||||
|
// YAML-Config in Temp-Datei schreiben
|
||||||
|
String yaml =
|
||||||
|
""
|
||||||
|
+ "ui:\n"
|
||||||
|
+ " appName: \"My App\"\n"
|
||||||
|
+ "system:\n"
|
||||||
|
+ " enableAnalytics: true\n";
|
||||||
|
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
|
||||||
|
Files.writeString(tmp, yaml);
|
||||||
|
|
||||||
|
// Pfad per statischem Mock liefern
|
||||||
|
try (MockedStatic<InstallationPathConfig> mocked =
|
||||||
|
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||||
|
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
|
||||||
|
|
||||||
|
ConfigurableEnvironment env = new StandardEnvironment();
|
||||||
|
ApplicationProperties props = new ApplicationProperties();
|
||||||
|
|
||||||
|
props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein
|
||||||
|
|
||||||
|
assertEquals("My App", env.getProperty("ui.appName"));
|
||||||
|
assertEquals("true", env.getProperty("system.enableAnalytics"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throws_when_settings_file_missing() throws Exception {
|
||||||
|
String missing = "/path/does/not/exist/spdf.yml";
|
||||||
|
try (MockedStatic<InstallationPathConfig> mocked =
|
||||||
|
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||||
|
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(missing);
|
||||||
|
|
||||||
|
ConfigurableEnvironment env = new StandardEnvironment();
|
||||||
|
ApplicationProperties props = new ApplicationProperties();
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> props.dynamicYamlPropertySource(env));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Driver;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Premium;
|
||||||
|
import stirling.software.common.model.ApplicationProperties.Security;
|
||||||
|
import stirling.software.common.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
|
class ApplicationPropertiesLogicTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void system_isAnalyticsEnabled_null_false_true() {
|
||||||
|
ApplicationProperties.System sys = new ApplicationProperties.System();
|
||||||
|
|
||||||
|
sys.setEnableAnalytics(null);
|
||||||
|
assertFalse(sys.isAnalyticsEnabled());
|
||||||
|
|
||||||
|
sys.setEnableAnalytics(Boolean.FALSE);
|
||||||
|
assertFalse(sys.isAnalyticsEnabled());
|
||||||
|
|
||||||
|
sys.setEnableAnalytics(Boolean.TRUE);
|
||||||
|
assertTrue(sys.isAnalyticsEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tempFileManagement_defaults_and_overrides() {
|
||||||
|
ApplicationProperties.TempFileManagement tfm =
|
||||||
|
new ApplicationProperties.TempFileManagement();
|
||||||
|
|
||||||
|
String expectedBase =
|
||||||
|
java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "")
|
||||||
|
+ "/stirling-pdf";
|
||||||
|
assertEquals(expectedBase, tfm.getBaseTmpDir());
|
||||||
|
|
||||||
|
String expectedLibre = expectedBase + "/libreoffice";
|
||||||
|
assertEquals(expectedLibre, tfm.getLibreofficeDir());
|
||||||
|
|
||||||
|
tfm.setBaseTmpDir("/custom/base");
|
||||||
|
assertEquals("/custom/base", tfm.getBaseTmpDir());
|
||||||
|
|
||||||
|
tfm.setLibreofficeDir("/opt/libre");
|
||||||
|
assertEquals("/opt/libre", tfm.getLibreofficeDir());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oauth2_scope_parsing_and_validity() {
|
||||||
|
Security.OAUTH2 oauth2 = new Security.OAUTH2();
|
||||||
|
oauth2.setIssuer("https://issuer");
|
||||||
|
oauth2.setClientId("client");
|
||||||
|
oauth2.setClientSecret("secret");
|
||||||
|
oauth2.setUseAsUsername("email");
|
||||||
|
oauth2.setScopes("openid, profile ,email");
|
||||||
|
assertTrue(oauth2.isSettingsValid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void security_login_method_flags() {
|
||||||
|
Security sec = new Security();
|
||||||
|
|
||||||
|
sec.getOauth2().setEnabled(true);
|
||||||
|
sec.getSaml2().setEnabled(true);
|
||||||
|
|
||||||
|
assertTrue(sec.isUserPass());
|
||||||
|
assertTrue(sec.isOauth2Active());
|
||||||
|
assertTrue(sec.isSaml2Active());
|
||||||
|
|
||||||
|
sec.setLoginMethod(Security.LoginMethods.NORMAL.toString());
|
||||||
|
assertTrue(sec.isUserPass());
|
||||||
|
assertFalse(sec.isOauth2Active());
|
||||||
|
assertFalse(sec.isSaml2Active());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void security_isAltLogin_reflects_oauth2_or_saml2() {
|
||||||
|
Security sec = new Security();
|
||||||
|
|
||||||
|
assertFalse(sec.isAltLogin());
|
||||||
|
|
||||||
|
sec.getOauth2().setEnabled(true);
|
||||||
|
sec.getSaml2().setEnabled(false);
|
||||||
|
assertTrue(sec.isAltLogin());
|
||||||
|
|
||||||
|
sec.getOauth2().setEnabled(false);
|
||||||
|
sec.getSaml2().setEnabled(true);
|
||||||
|
assertTrue(sec.isAltLogin());
|
||||||
|
|
||||||
|
sec.getOauth2().setEnabled(true);
|
||||||
|
sec.getSaml2().setEnabled(true);
|
||||||
|
assertTrue(sec.isAltLogin());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void oauth2_client_provider_mapping_and_unsupported() throws UnsupportedProviderException {
|
||||||
|
Security.OAUTH2.Client client = new Security.OAUTH2.Client();
|
||||||
|
|
||||||
|
assertNotNull(client.get("google"));
|
||||||
|
assertNotNull(client.get("github"));
|
||||||
|
assertNotNull(client.get("keycloak"));
|
||||||
|
|
||||||
|
UnsupportedProviderException ex =
|
||||||
|
assertThrows(UnsupportedProviderException.class, () -> client.get("unknown"));
|
||||||
|
assertTrue(ex.getMessage().toLowerCase().contains("not supported"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void premium_google_drive_getters_return_empty_string_on_null_or_blank() {
|
||||||
|
Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive();
|
||||||
|
|
||||||
|
assertEquals("", gd.getClientId());
|
||||||
|
assertEquals("", gd.getApiKey());
|
||||||
|
assertEquals("", gd.getAppId());
|
||||||
|
|
||||||
|
gd.setClientId(" id ");
|
||||||
|
gd.setApiKey(" key ");
|
||||||
|
gd.setAppId(" app ");
|
||||||
|
assertEquals(" id ", gd.getClientId());
|
||||||
|
assertEquals(" key ", gd.getApiKey());
|
||||||
|
assertEquals(" app ", gd.getAppId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void ui_getters_return_null_for_blank() {
|
||||||
|
ApplicationProperties.Ui ui = new ApplicationProperties.Ui();
|
||||||
|
ui.setAppName(" ");
|
||||||
|
ui.setHomeDescription("");
|
||||||
|
ui.setAppNameNavbar(null);
|
||||||
|
|
||||||
|
assertNull(ui.getAppName());
|
||||||
|
assertNull(ui.getHomeDescription());
|
||||||
|
assertNull(ui.getAppNameNavbar());
|
||||||
|
|
||||||
|
ui.setAppName("Stirling-PDF");
|
||||||
|
ui.setHomeDescription("Home");
|
||||||
|
ui.setAppNameNavbar("Nav");
|
||||||
|
assertEquals("Stirling-PDF", ui.getAppName());
|
||||||
|
assertEquals("Home", ui.getHomeDescription());
|
||||||
|
assertEquals("Nav", ui.getAppNameNavbar());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void driver_toString_contains_driver_name() {
|
||||||
|
assertTrue(Driver.H2.toString().contains("h2"));
|
||||||
|
assertTrue(Driver.POSTGRESQL.toString().contains("postgresql"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void session_limits_and_timeouts_have_reasonable_defaults() {
|
||||||
|
ApplicationProperties.ProcessExecutor pe = new ApplicationProperties.ProcessExecutor();
|
||||||
|
|
||||||
|
ApplicationProperties.ProcessExecutor.SessionLimit s = pe.getSessionLimit();
|
||||||
|
assertEquals(2, s.getQpdfSessionLimit());
|
||||||
|
assertEquals(1, s.getTesseractSessionLimit());
|
||||||
|
assertEquals(1, s.getLibreOfficeSessionLimit());
|
||||||
|
assertEquals(1, s.getPdfToHtmlSessionLimit());
|
||||||
|
assertEquals(8, s.getPythonOpenCvSessionLimit());
|
||||||
|
assertEquals(16, s.getWeasyPrintSessionLimit());
|
||||||
|
assertEquals(1, s.getInstallAppSessionLimit());
|
||||||
|
assertEquals(1, s.getCalibreSessionLimit());
|
||||||
|
assertEquals(8, s.getGhostscriptSessionLimit());
|
||||||
|
assertEquals(2, s.getOcrMyPdfSessionLimit());
|
||||||
|
|
||||||
|
ApplicationProperties.ProcessExecutor.TimeoutMinutes t = pe.getTimeoutMinutes();
|
||||||
|
assertEquals(30, t.getTesseractTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getQpdfTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getLibreOfficeTimeoutMinutes());
|
||||||
|
assertEquals(20, t.getPdfToHtmlTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getPythonOpenCvTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getWeasyPrintTimeoutMinutes());
|
||||||
|
assertEquals(60, t.getInstallAppTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getCalibreTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getGhostscriptTimeoutMinutes());
|
||||||
|
assertEquals(30, t.getOcrMyPdfTimeoutMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
@Test
|
||||||
|
void enterprise_metadata_defaults() {
|
||||||
|
ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition();
|
||||||
|
ApplicationProperties.EnterpriseEdition.CustomMetadata eMeta = ee.getCustomMetadata();
|
||||||
|
eMeta.setCreator(" ");
|
||||||
|
eMeta.setProducer(null);
|
||||||
|
assertEquals("Stirling-PDF", eMeta.getCreator());
|
||||||
|
assertEquals("Stirling-PDF", eMeta.getProducer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void premium_metadata_defaults() {
|
||||||
|
Premium.ProFeatures pf = new Premium.ProFeatures();
|
||||||
|
Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata();
|
||||||
|
pMeta.setCreator("");
|
||||||
|
pMeta.setProducer("");
|
||||||
|
assertEquals("Stirling-PDF", pMeta.getCreator());
|
||||||
|
assertEquals("Stirling-PDF", pMeta.getProducer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void premium_metadata_awesome() {
|
||||||
|
Premium.ProFeatures pf = new Premium.ProFeatures();
|
||||||
|
Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata();
|
||||||
|
pMeta.setCreator("Awesome PDF Tool");
|
||||||
|
pMeta.setProducer("Awesome PDF Tool");
|
||||||
|
assertEquals("Awesome PDF Tool", pMeta.getCreator());
|
||||||
|
assertEquals("Awesome PDF Tool", pMeta.getProducer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void string_isValid_handles_null_empty_blank_and_trimmed() {
|
||||||
|
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
|
||||||
|
|
||||||
|
assertFalse(oauth2.isValid((String) null, "issuer"));
|
||||||
|
assertFalse(oauth2.isValid("", "issuer"));
|
||||||
|
assertFalse(oauth2.isValid(" ", "issuer"));
|
||||||
|
|
||||||
|
assertTrue(oauth2.isValid("x", "issuer"));
|
||||||
|
assertTrue(oauth2.isValid(" x ", "issuer")); // trimmt intern
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collection_isValid_handles_null_and_empty() {
|
||||||
|
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
|
||||||
|
|
||||||
|
Collection<String> nullColl = null;
|
||||||
|
Collection<String> empty = List.of();
|
||||||
|
|
||||||
|
assertFalse(oauth2.isValid(nullColl, "scopes"));
|
||||||
|
assertFalse(oauth2.isValid(empty, "scopes"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void collection_isValid_true_when_non_empty_even_if_element_is_blank() {
|
||||||
|
ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2();
|
||||||
|
|
||||||
|
// Aktuelles Verhalten: prüft NUR !isEmpty(), nicht Inhalt
|
||||||
|
Collection<String> oneBlank = new ArrayList<>();
|
||||||
|
oneBlank.add(" ");
|
||||||
|
|
||||||
|
assertTrue(
|
||||||
|
oauth2.isValid(oneBlank, "scopes"),
|
||||||
|
"Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn Element leer/blank ist");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
import okhttp3.mockwebserver.MockResponse;
|
||||||
|
import okhttp3.mockwebserver.MockWebServer;
|
||||||
|
|
||||||
|
class ApplicationPropertiesSaml2HttpTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idpMetadataUri_http_is_resolved_via_mockwebserver() throws Exception {
|
||||||
|
try (MockWebServer server = new MockWebServer()) {
|
||||||
|
server.enqueue(
|
||||||
|
new MockResponse()
|
||||||
|
.setResponseCode(200)
|
||||||
|
.addHeader("Content-Type", "application/xml")
|
||||||
|
.setBody("<EntityDescriptor/>"));
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
String url = server.url("/meta").toString();
|
||||||
|
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
s.setIdpMetadataUri(url);
|
||||||
|
|
||||||
|
try (InputStream in = s.getIdpMetadataUri()) {
|
||||||
|
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(body.contains("EntityDescriptor"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idpMetadataUri_invalidUri_triggers_catch_and_throwsIOException() {
|
||||||
|
// Ungültige URI -> new URI(...) wirft URISyntaxException -> catch -> IOException
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
s.setIdpMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##)
|
||||||
|
|
||||||
|
assertThrows(IOException.class, s::getIdpMetadataUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void spCert_else_branch_returns_FileSystemResource_for_filesystem_path() throws Exception {
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
|
||||||
|
// temporäre Datei simuliert "Filesystem"-Pfad (-> else-Zweig)
|
||||||
|
Path tmp = Files.createTempFile("spdf-spcert-", ".crt");
|
||||||
|
Files.writeString(tmp, "CERT");
|
||||||
|
|
||||||
|
s.setSpCert(tmp.toString());
|
||||||
|
Resource r = s.getSpCert();
|
||||||
|
|
||||||
|
assertNotNull(r);
|
||||||
|
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
||||||
|
assertTrue(r.exists(), "Temp file should exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idpCert_else_branch_returns_FileSystemResource_even_if_missing() {
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
|
||||||
|
// bewusst nicht existierender Pfad -> else-Zweig wird trotzdem genommen
|
||||||
|
String missing = "/this/path/does/not/exist/idp.crt";
|
||||||
|
s.setIdpCert(missing);
|
||||||
|
Resource r = s.getIdpCert();
|
||||||
|
|
||||||
|
assertNotNull(r);
|
||||||
|
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
||||||
|
assertFalse(r.exists(), "Resource should not exist for missing file");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package stirling.software.common.model;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
class ApplicationPropertiesSaml2ResourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idpMetadataUri_classpath_is_resolved() throws Exception {
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
s.setIdpMetadataUri("classpath:saml/dummy.txt");
|
||||||
|
|
||||||
|
try (InputStream in = s.getIdpMetadataUri()) {
|
||||||
|
assertNotNull(in, "Classpath InputStream should not be null");
|
||||||
|
String txt = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(txt.contains("ok"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void spCert_idpCert_privateKey_null_classpath_and_filesystem() throws Exception {
|
||||||
|
var s = new ApplicationProperties.Security.SAML2();
|
||||||
|
|
||||||
|
s.setSpCert(null);
|
||||||
|
s.setIdpCert(null);
|
||||||
|
s.setPrivateKey(null);
|
||||||
|
assertNull(s.getSpCert());
|
||||||
|
assertNull(s.getIdpCert());
|
||||||
|
assertNull(s.getPrivateKey());
|
||||||
|
|
||||||
|
s.setSpCert("classpath:saml/dummy.txt");
|
||||||
|
s.setIdpCert("classpath:saml/dummy.txt");
|
||||||
|
s.setPrivateKey("classpath:saml/dummy.txt");
|
||||||
|
Resource sp = s.getSpCert();
|
||||||
|
Resource idp = s.getIdpCert();
|
||||||
|
Resource pk = s.getPrivateKey();
|
||||||
|
assertTrue(sp.exists());
|
||||||
|
assertTrue(idp.exists());
|
||||||
|
assertTrue(pk.exists());
|
||||||
|
|
||||||
|
Path tmp = Files.createTempFile("spdf-key-", ".pem");
|
||||||
|
Files.writeString(tmp, "KEY");
|
||||||
|
s.setPrivateKey(tmp.toString());
|
||||||
|
Resource pkFs = s.getPrivateKey();
|
||||||
|
assertNotNull(pkFs);
|
||||||
|
assertTrue(pkFs.exists());
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,9 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
import static org.mockito.AdditionalAnswers.*;
|
import static org.mockito.AdditionalAnswers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
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;
|
||||||
@ -21,14 +20,11 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
class FileStorageTest {
|
class FileStorageTest {
|
||||||
|
|
||||||
@TempDir
|
@TempDir Path tempDir;
|
||||||
Path tempDir;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private FileOrUploadService fileOrUploadService;
|
||||||
private FileOrUploadService fileOrUploadService;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks private FileStorage fileStorage;
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
private MultipartFile mockFile;
|
private MultipartFile mockFile;
|
||||||
|
|
||||||
@ -50,11 +46,14 @@ class FileStorageTest {
|
|||||||
when(mockFile.getBytes()).thenReturn(fileContent);
|
when(mockFile.getBytes()).thenReturn(fileContent);
|
||||||
|
|
||||||
// Set up mock to handle transferTo by writing the file
|
// Set up mock to handle transferTo by writing the file
|
||||||
doAnswer(invocation -> {
|
doAnswer(
|
||||||
java.io.File file = invocation.getArgument(0);
|
invocation -> {
|
||||||
Files.write(file.toPath(), fileContent);
|
java.io.File file = invocation.getArgument(0);
|
||||||
return null;
|
Files.write(file.toPath(), fileContent);
|
||||||
}).when(mockFile).transferTo(any(java.io.File.class));
|
return null;
|
||||||
|
})
|
||||||
|
.when(mockFile)
|
||||||
|
.transferTo(any(java.io.File.class));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String fileId = fileStorage.storeFile(mockFile);
|
String fileId = fileStorage.storeFile(mockFile);
|
||||||
@ -90,7 +89,7 @@ class FileStorageTest {
|
|||||||
|
|
||||||
MultipartFile expectedFile = mock(MultipartFile.class);
|
MultipartFile expectedFile = mock(MultipartFile.class);
|
||||||
when(fileOrUploadService.toMockMultipartFile(eq(fileId), eq(fileContent)))
|
when(fileOrUploadService.toMockMultipartFile(eq(fileId), eq(fileContent)))
|
||||||
.thenReturn(expectedFile);
|
.thenReturn(expectedFile);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
MultipartFile result = fileStorage.retrieveFile(fileId);
|
MultipartFile result = fileStorage.retrieveFile(fileId);
|
||||||
|
@ -4,14 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.never;
|
|
||||||
import static org.mockito.Mockito.times;
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ -30,11 +25,9 @@ import org.mockito.Mockito;
|
|||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobProgress;
|
|
||||||
import stirling.software.common.model.job.JobResponse;
|
import stirling.software.common.model.job.JobResponse;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@ -42,36 +35,31 @@ class JobExecutorServiceTest {
|
|||||||
|
|
||||||
private JobExecutorService jobExecutorService;
|
private JobExecutorService jobExecutorService;
|
||||||
|
|
||||||
@Mock
|
@Mock private TaskManager taskManager;
|
||||||
private TaskManager taskManager;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private FileStorage fileStorage;
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private HttpServletRequest request;
|
||||||
private HttpServletRequest request;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private ResourceMonitor resourceMonitor;
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private JobQueue jobQueue;
|
||||||
private JobQueue jobQueue;
|
|
||||||
|
|
||||||
@Captor
|
@Captor private ArgumentCaptor<String> jobIdCaptor;
|
||||||
private ArgumentCaptor<String> jobIdCaptor;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
// Initialize the service manually with all its dependencies
|
// Initialize the service manually with all its dependencies
|
||||||
jobExecutorService = new JobExecutorService(
|
jobExecutorService =
|
||||||
taskManager,
|
new JobExecutorService(
|
||||||
fileStorage,
|
taskManager,
|
||||||
request,
|
fileStorage,
|
||||||
resourceMonitor,
|
request,
|
||||||
jobQueue,
|
resourceMonitor,
|
||||||
30000L, // asyncRequestTimeoutMs
|
jobQueue,
|
||||||
"30m" // sessionTimeout
|
30000L, // asyncRequestTimeoutMs
|
||||||
);
|
"30m" // sessionTimeout
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -109,13 +97,13 @@ class JobExecutorServiceTest {
|
|||||||
verify(taskManager).createTask(jobIdCaptor.capture());
|
verify(taskManager).createTask(jobIdCaptor.capture());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldHandleSyncJobError() {
|
void shouldHandleSyncJobError() {
|
||||||
// Given
|
// Given
|
||||||
Supplier<Object> work = () -> {
|
Supplier<Object> work =
|
||||||
throw new RuntimeException("Test error");
|
() -> {
|
||||||
};
|
throw new RuntimeException("Test error");
|
||||||
|
};
|
||||||
|
|
||||||
// When
|
// When
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(false, work);
|
||||||
@ -141,8 +129,7 @@ class JobExecutorServiceTest {
|
|||||||
when(jobQueue.queueJob(anyString(), eq(80), any(), anyLong())).thenReturn(future);
|
when(jobQueue.queueJob(anyString(), eq(80), any(), anyLong())).thenReturn(future);
|
||||||
|
|
||||||
// When
|
// When
|
||||||
ResponseEntity<?> response = jobExecutorService.runJobGeneric(
|
ResponseEntity<?> response = jobExecutorService.runJobGeneric(true, work, 5000, true, 80);
|
||||||
true, work, 5000, true, 80);
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
@ -160,8 +147,9 @@ class JobExecutorServiceTest {
|
|||||||
long customTimeout = 60000L;
|
long customTimeout = 60000L;
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
// Use reflection to access the private executeWithTimeout method
|
||||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
java.lang.reflect.Method executeMethod =
|
||||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
JobExecutorService.class.getDeclaredMethod(
|
||||||
|
"executeWithTimeout", Supplier.class, long.class);
|
||||||
executeMethod.setAccessible(true);
|
executeMethod.setAccessible(true);
|
||||||
|
|
||||||
// Create a spy on the JobExecutorService to verify method calls
|
// Create a spy on the JobExecutorService to verify method calls
|
||||||
@ -177,19 +165,21 @@ class JobExecutorServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldHandleTimeout() throws Exception {
|
void shouldHandleTimeout() throws Exception {
|
||||||
// Given
|
// Given
|
||||||
Supplier<Object> work = () -> {
|
Supplier<Object> work =
|
||||||
try {
|
() -> {
|
||||||
Thread.sleep(100); // Simulate long-running job
|
try {
|
||||||
return "test-result";
|
Thread.sleep(100); // Simulate long-running job
|
||||||
} catch (InterruptedException e) {
|
return "test-result";
|
||||||
Thread.currentThread().interrupt();
|
} catch (InterruptedException e) {
|
||||||
throw new RuntimeException(e);
|
Thread.currentThread().interrupt();
|
||||||
}
|
throw new RuntimeException(e);
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Use reflection to access the private executeWithTimeout method
|
// Use reflection to access the private executeWithTimeout method
|
||||||
java.lang.reflect.Method executeMethod = JobExecutorService.class
|
java.lang.reflect.Method executeMethod =
|
||||||
.getDeclaredMethod("executeWithTimeout", Supplier.class, long.class);
|
JobExecutorService.class.getDeclaredMethod(
|
||||||
|
"executeWithTimeout", Supplier.class, long.class);
|
||||||
executeMethod.setAccessible(true);
|
executeMethod.setAccessible(true);
|
||||||
|
|
||||||
// When/Then
|
// When/Then
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -17,7 +15,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import stirling.software.common.model.job.JobProgress;
|
|
||||||
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@ -25,16 +22,17 @@ class JobQueueTest {
|
|||||||
|
|
||||||
private JobQueue jobQueue;
|
private JobQueue jobQueue;
|
||||||
|
|
||||||
@Mock
|
@Mock private ResourceMonitor resourceMonitor;
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
|
private final AtomicReference<ResourceStatus> statusRef =
|
||||||
private final AtomicReference<ResourceStatus> statusRef = new AtomicReference<>(ResourceStatus.OK);
|
new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
// Mark stubbing as lenient to avoid UnnecessaryStubbingException
|
// Mark stubbing as lenient to avoid UnnecessaryStubbingException
|
||||||
lenient().when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt())).thenReturn(10);
|
lenient()
|
||||||
|
.when(resourceMonitor.calculateDynamicQueueCapacity(anyInt(), anyInt()))
|
||||||
|
.thenReturn(10);
|
||||||
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
lenient().when(resourceMonitor.getCurrentStatus()).thenReturn(statusRef);
|
||||||
|
|
||||||
// Initialize JobQueue with mocked ResourceMonitor
|
// Initialize JobQueue with mocked ResourceMonitor
|
||||||
@ -50,7 +48,6 @@ class JobQueueTest {
|
|||||||
|
|
||||||
jobQueue.queueJob(jobId, resourceWeight, work, timeoutMs);
|
jobQueue.queueJob(jobId, resourceWeight, work, timeoutMs);
|
||||||
|
|
||||||
|
|
||||||
assertTrue(jobQueue.isJobQueued(jobId));
|
assertTrue(jobQueue.isJobQueued(jobId));
|
||||||
assertEquals(1, jobQueue.getTotalQueuedJobs());
|
assertEquals(1, jobQueue.getTotalQueuedJobs());
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
package stirling.software.common.service;
|
package stirling.software.common.service;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import java.lang.management.MemoryMXBean;
|
import java.lang.management.MemoryMXBean;
|
||||||
import java.lang.management.MemoryUsage;
|
|
||||||
import java.lang.management.OperatingSystemMXBean;
|
import java.lang.management.OperatingSystemMXBean;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@ -30,20 +26,19 @@ import stirling.software.common.service.ResourceMonitor.ResourceStatus;
|
|||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ResourceMonitorTest {
|
class ResourceMonitorTest {
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks private ResourceMonitor resourceMonitor;
|
||||||
private ResourceMonitor resourceMonitor;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private OperatingSystemMXBean osMXBean;
|
||||||
private OperatingSystemMXBean osMXBean;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private MemoryMXBean memoryMXBean;
|
||||||
private MemoryMXBean memoryMXBean;
|
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
private AtomicReference<ResourceStatus> currentStatus = new AtomicReference<>(ResourceStatus.OK);
|
private AtomicReference<ResourceStatus> currentStatus =
|
||||||
|
new AtomicReference<>(ResourceStatus.OK);
|
||||||
|
|
||||||
@Spy
|
@Spy
|
||||||
private AtomicReference<ResourceMetrics> latestMetrics = new AtomicReference<>(new ResourceMetrics());
|
private AtomicReference<ResourceMetrics> latestMetrics =
|
||||||
|
new AtomicReference<>(new ResourceMetrics());
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@ -92,23 +87,26 @@ class ResourceMonitorTest {
|
|||||||
assertEquals(3, capacity, "With CRITICAL status, capacity should be reduced to 30%");
|
assertEquals(3, capacity, "With CRITICAL status, capacity should be reduced to 30%");
|
||||||
|
|
||||||
// Test minimum capacity enforcement
|
// Test minimum capacity enforcement
|
||||||
assertEquals(minCapacity, resourceMonitor.calculateDynamicQueueCapacity(1, minCapacity),
|
assertEquals(
|
||||||
|
minCapacity,
|
||||||
|
resourceMonitor.calculateDynamicQueueCapacity(1, minCapacity),
|
||||||
"Should never go below minimum capacity");
|
"Should never go below minimum capacity");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({
|
@CsvSource({
|
||||||
"10, OK, false", // Light job, OK status
|
"10, OK, false", // Light job, OK status
|
||||||
"10, WARNING, false", // Light job, WARNING status
|
"10, WARNING, false", // Light job, WARNING status
|
||||||
"10, CRITICAL, true", // Light job, CRITICAL status
|
"10, CRITICAL, true", // Light job, CRITICAL status
|
||||||
"30, OK, false", // Medium job, OK status
|
"30, OK, false", // Medium job, OK status
|
||||||
"30, WARNING, true", // Medium job, WARNING status
|
"30, WARNING, true", // Medium job, WARNING status
|
||||||
"30, CRITICAL, true", // Medium job, CRITICAL status
|
"30, CRITICAL, true", // Medium job, CRITICAL status
|
||||||
"80, OK, true", // Heavy job, OK status
|
"80, OK, true", // Heavy job, OK status
|
||||||
"80, WARNING, true", // Heavy job, WARNING status
|
"80, WARNING, true", // Heavy job, WARNING status
|
||||||
"80, CRITICAL, true" // Heavy job, CRITICAL status
|
"80, CRITICAL, true" // Heavy job, CRITICAL status
|
||||||
})
|
})
|
||||||
void shouldQueueJobBasedOnWeightAndStatus(int weight, ResourceStatus status, boolean shouldQueue) {
|
void shouldQueueJobBasedOnWeightAndStatus(
|
||||||
|
int weight, ResourceStatus status, boolean shouldQueue) {
|
||||||
// Given
|
// Given
|
||||||
currentStatus.set(status);
|
currentStatus.set(status);
|
||||||
|
|
||||||
@ -116,8 +114,11 @@ class ResourceMonitorTest {
|
|||||||
boolean result = resourceMonitor.shouldQueueJob(weight);
|
boolean result = resourceMonitor.shouldQueueJob(weight);
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals(shouldQueue, result,
|
assertEquals(
|
||||||
String.format("For weight %d and status %s, shouldQueue should be %s",
|
shouldQueue,
|
||||||
|
result,
|
||||||
|
String.format(
|
||||||
|
"For weight %d and status %s, shouldQueue should be %s",
|
||||||
weight, status, shouldQueue));
|
weight, status, shouldQueue));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +132,9 @@ class ResourceMonitorTest {
|
|||||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
||||||
|
|
||||||
// When/Then
|
// When/Then
|
||||||
assertTrue(staleMetrics.isStale(5000), "Metrics from 6 seconds ago should be stale with 5s threshold");
|
assertTrue(
|
||||||
|
staleMetrics.isStale(5000),
|
||||||
|
"Metrics from 6 seconds ago should be stale with 5s threshold");
|
||||||
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
assertFalse(freshMetrics.isStale(5000), "Fresh metrics should not be stale");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,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 java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@ -22,11 +21,9 @@ import stirling.software.common.model.job.ResultFile;
|
|||||||
|
|
||||||
class TaskManagerTest {
|
class TaskManagerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock private FileStorage fileStorage;
|
||||||
private FileStorage fileStorage;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks private TaskManager taskManager;
|
||||||
private TaskManager taskManager;
|
|
||||||
|
|
||||||
private AutoCloseable closeable;
|
private AutoCloseable closeable;
|
||||||
|
|
||||||
@ -234,18 +231,20 @@ class TaskManagerTest {
|
|||||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||||
|
|
||||||
// Create a ResultFile and set it using the new approach
|
// Create a ResultFile and set it using the new approach
|
||||||
ResultFile resultFile = ResultFile.builder()
|
ResultFile resultFile =
|
||||||
.fileId("file-id")
|
ResultFile.builder()
|
||||||
.fileName("test.pdf")
|
.fileId("file-id")
|
||||||
.contentType("application/pdf")
|
.fileName("test.pdf")
|
||||||
.fileSize(1024L)
|
.contentType("application/pdf")
|
||||||
.build();
|
.fileSize(1024L)
|
||||||
|
.build();
|
||||||
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
ReflectionTestUtils.setField(oldJob, "resultFiles", java.util.List.of(resultFile));
|
||||||
|
|
||||||
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
when(fileStorage.deleteFile("file-id")).thenReturn(true);
|
||||||
|
|
||||||
// Obtain access to the private jobResults map
|
// Obtain access to the private jobResults map
|
||||||
Map<String, JobResult> jobResultsMap = (Map<String, JobResult>) ReflectionTestUtils.getField(taskManager, "jobResults");
|
Map<String, JobResult> jobResultsMap =
|
||||||
|
(Map<String, JobResult>) ReflectionTestUtils.getField(taskManager, "jobResults");
|
||||||
|
|
||||||
// 3. Create an active job
|
// 3. Create an active job
|
||||||
String activeJobId = "active-job";
|
String activeJobId = "active-job";
|
||||||
|
@ -12,7 +12,6 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.attribute.FileTime;
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
@ -30,31 +29,22 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.TempFileRegistry;
|
import stirling.software.common.util.TempFileRegistry;
|
||||||
|
|
||||||
/**
|
/** Tests for the TempFileCleanupService, focusing on its pattern-matching and cleanup logic. */
|
||||||
* Tests for the TempFileCleanupService, focusing on its pattern-matching and cleanup logic.
|
|
||||||
*/
|
|
||||||
public class TempFileCleanupServiceTest {
|
public class TempFileCleanupServiceTest {
|
||||||
|
|
||||||
@TempDir
|
@TempDir Path tempDir;
|
||||||
Path tempDir;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private TempFileRegistry registry;
|
||||||
private TempFileRegistry registry;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private TempFileManager tempFileManager;
|
||||||
private TempFileManager tempFileManager;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private ApplicationProperties applicationProperties;
|
||||||
private ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private ApplicationProperties.System system;
|
||||||
private ApplicationProperties.System system;
|
|
||||||
|
|
||||||
@Mock
|
@Mock private ApplicationProperties.TempFileManagement tempFileManagement;
|
||||||
private ApplicationProperties.TempFileManagement tempFileManagement;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks private TempFileCleanupService cleanupService;
|
||||||
private TempFileCleanupService cleanupService;
|
|
||||||
|
|
||||||
private Path systemTempDir;
|
private Path systemTempDir;
|
||||||
private Path customTempDir;
|
private Path customTempDir;
|
||||||
@ -124,7 +114,8 @@ public class TempFileCleanupServiceTest {
|
|||||||
|
|
||||||
// Files that should be preserved
|
// Files that should be preserved
|
||||||
Path jettyFile1 = Files.createFile(systemTempDir.resolve("jetty-123.tmp"));
|
Path jettyFile1 = Files.createFile(systemTempDir.resolve("jetty-123.tmp"));
|
||||||
Path jettyFile2 = Files.createFile(systemTempDir.resolve("something-with-jetty-inside.tmp"));
|
Path jettyFile2 =
|
||||||
|
Files.createFile(systemTempDir.resolve("something-with-jetty-inside.tmp"));
|
||||||
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
Path regularFile = Files.createFile(systemTempDir.resolve("important.txt"));
|
||||||
|
|
||||||
// Create a nested directory with temp files
|
// Create a nested directory with temp files
|
||||||
@ -143,19 +134,29 @@ 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)) {
|
||||||
// Mock Files.list for each directory we'll process
|
// Mock Files.list for each directory we'll process
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
.thenReturn(Stream.of(
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
ourTempFile1, ourTempFile2, oldTempFile, sysTempFile1,
|
.thenReturn(
|
||||||
jettyFile1, jettyFile2, regularFile, emptyFile, nestedDir));
|
Stream.of(
|
||||||
|
ourTempFile1,
|
||||||
|
ourTempFile2,
|
||||||
|
oldTempFile,
|
||||||
|
sysTempFile1,
|
||||||
|
jettyFile1,
|
||||||
|
jettyFile2,
|
||||||
|
regularFile,
|
||||||
|
emptyFile,
|
||||||
|
nestedDir));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(customTempDir)))
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(customTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
.thenReturn(Stream.of(ourTempFile3, ourTempFile4, sysTempFile2, sysTempFile3));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(libreOfficeTempDir)))
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(libreOfficeTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile5));
|
.thenReturn(Stream.of(ourTempFile5));
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(nestedDir)))
|
mockedFiles.when(() -> Files.list(eq(nestedDir))).thenReturn(Stream.of(nestedTempFile));
|
||||||
.thenReturn(Stream.of(nestedTempFile));
|
|
||||||
|
|
||||||
// Configure Files.isDirectory for each path
|
// Configure Files.isDirectory for each path
|
||||||
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
mockedFiles.when(() -> Files.isDirectory(eq(nestedDir))).thenReturn(true);
|
||||||
@ -165,48 +166,59 @@ public class TempFileCleanupServiceTest {
|
|||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
String fileName = path.getFileName().toString();
|
invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
// For files with "old" in the name, return a timestamp older than maxAgeMillis
|
// For files with "old" in the name, return a timestamp older than
|
||||||
if (fileName.contains("old")) {
|
// maxAgeMillis
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
if (fileName.contains("old")) {
|
||||||
}
|
return FileTime.fromMillis(
|
||||||
// For empty.tmp file, return a timestamp older than 5 minutes (for empty file test)
|
System.currentTimeMillis() - 5000000);
|
||||||
else if (fileName.equals("empty.tmp")) {
|
}
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
||||||
}
|
// empty file test)
|
||||||
// For all other files, return a recent timestamp
|
else if (fileName.equals("empty.tmp")) {
|
||||||
else {
|
return FileTime.fromMillis(
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000); // 1 minute ago
|
System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
}
|
}
|
||||||
});
|
// For all other files, return a recent timestamp
|
||||||
|
else {
|
||||||
|
return FileTime.fromMillis(
|
||||||
|
System.currentTimeMillis() - 60000); // 1 minute ago
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Configure Files.size to return different sizes based on file names
|
// Configure Files.size to return different sizes based on file names
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.size(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
String fileName = path.getFileName().toString();
|
invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
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 (fileName.equals("empty.tmp")) {
|
||||||
return 0L;
|
return 0L;
|
||||||
}
|
}
|
||||||
// Return normal size for all other files
|
// Return normal size for all other files
|
||||||
else {
|
else {
|
||||||
return 1024L; // 1 KB
|
return 1024L; // 1 KB
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
// For deleteIfExists, track which files would be deleted
|
||||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
deletedFiles.add(path);
|
invocation -> {
|
||||||
return true;
|
Path path = invocation.getArgument(0);
|
||||||
});
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Act - set containerMode to false for this test
|
// Act - set containerMode to false for this test
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
@ -218,20 +230,33 @@ public class TempFileCleanupServiceTest {
|
|||||||
assertTrue(deletedFiles.contains(emptyFile), "Empty file should be deleted");
|
assertTrue(deletedFiles.contains(emptyFile), "Empty file should be deleted");
|
||||||
|
|
||||||
// Regular temp files should not be deleted because they're too new
|
// Regular temp files should not be deleted because they're too new
|
||||||
assertFalse(deletedFiles.contains(ourTempFile1), "Recent temp file should be preserved");
|
assertFalse(
|
||||||
assertFalse(deletedFiles.contains(ourTempFile2), "Recent temp file should be preserved");
|
deletedFiles.contains(ourTempFile1), "Recent temp file should be preserved");
|
||||||
assertFalse(deletedFiles.contains(ourTempFile3), "Recent temp file should be preserved");
|
assertFalse(
|
||||||
assertFalse(deletedFiles.contains(ourTempFile4), "Recent temp file should be preserved");
|
deletedFiles.contains(ourTempFile2), "Recent temp file should be preserved");
|
||||||
assertFalse(deletedFiles.contains(ourTempFile5), "Recent temp file should be preserved");
|
assertFalse(
|
||||||
|
deletedFiles.contains(ourTempFile3), "Recent temp file should be preserved");
|
||||||
|
assertFalse(
|
||||||
|
deletedFiles.contains(ourTempFile4), "Recent temp file should be preserved");
|
||||||
|
assertFalse(
|
||||||
|
deletedFiles.contains(ourTempFile5), "Recent temp file should be preserved");
|
||||||
|
|
||||||
// System temp files should not be deleted in non-container mode
|
// System temp files should not be deleted in non-container mode
|
||||||
assertFalse(deletedFiles.contains(sysTempFile1), "System temp file should be preserved in non-container mode");
|
assertFalse(
|
||||||
assertFalse(deletedFiles.contains(sysTempFile2), "System temp file should be preserved in non-container mode");
|
deletedFiles.contains(sysTempFile1),
|
||||||
assertFalse(deletedFiles.contains(sysTempFile3), "System temp file should be preserved in non-container mode");
|
"System temp file should be preserved in non-container mode");
|
||||||
|
assertFalse(
|
||||||
|
deletedFiles.contains(sysTempFile2),
|
||||||
|
"System temp file should be preserved in non-container mode");
|
||||||
|
assertFalse(
|
||||||
|
deletedFiles.contains(sysTempFile3),
|
||||||
|
"System temp file should be preserved in non-container mode");
|
||||||
|
|
||||||
// Jetty files and regular files should never be deleted
|
// Jetty files and regular files should never be deleted
|
||||||
assertFalse(deletedFiles.contains(jettyFile1), "Jetty file should be preserved");
|
assertFalse(deletedFiles.contains(jettyFile1), "Jetty file should be preserved");
|
||||||
assertFalse(deletedFiles.contains(jettyFile2), "File with jetty in name should be preserved");
|
assertFalse(
|
||||||
|
deletedFiles.contains(jettyFile2),
|
||||||
|
"File with jetty in name should be preserved");
|
||||||
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +277,8 @@ 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)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
.thenReturn(Stream.of(ourTempFile, sysTempFile, regularFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
@ -262,28 +288,37 @@ public class TempFileCleanupServiceTest {
|
|||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
mockedFiles
|
||||||
.thenReturn(FileTime.fromMillis(System.currentTimeMillis() - 60000)); // 1 minute ago
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
|
.thenReturn(
|
||||||
|
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)))
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
||||||
.thenReturn(1024L); // 1 KB
|
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
// For deleteIfExists, track which files would be deleted
|
||||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
deletedFiles.add(path);
|
invocation -> {
|
||||||
return true;
|
Path path = invocation.getArgument(0);
|
||||||
});
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
||||||
|
|
||||||
// Assert - In container mode, both our temp files and system temp files should be deleted
|
// Assert - In container mode, both our temp files and system temp files should be
|
||||||
|
// deleted
|
||||||
// regardless of age (when maxAgeMillis is 0)
|
// regardless of age (when maxAgeMillis is 0)
|
||||||
assertTrue(deletedFiles.contains(ourTempFile), "Our temp file should be deleted in container mode");
|
assertTrue(
|
||||||
assertTrue(deletedFiles.contains(sysTempFile), "System temp file should be deleted in container mode");
|
deletedFiles.contains(ourTempFile),
|
||||||
|
"Our temp file should be deleted in container mode");
|
||||||
|
assertTrue(
|
||||||
|
deletedFiles.contains(sysTempFile),
|
||||||
|
"System temp file should be deleted in container mode");
|
||||||
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
assertFalse(deletedFiles.contains(regularFile), "Regular file should be preserved");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -303,7 +338,8 @@ 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)) {
|
||||||
// Mock Files.list for systemTempDir
|
// Mock Files.list for systemTempDir
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles
|
||||||
|
.when(() -> Files.list(eq(systemTempDir)))
|
||||||
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
.thenReturn(Stream.of(emptyFile, recentEmptyFile));
|
||||||
|
|
||||||
// Configure Files.isDirectory
|
// Configure Files.isDirectory
|
||||||
@ -313,39 +349,46 @@ public class TempFileCleanupServiceTest {
|
|||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
String fileName = path.getFileName().toString();
|
invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.equals("empty.tmp")) {
|
if (fileName.equals("empty.tmp")) {
|
||||||
// More than 5 minutes old
|
// More than 5 minutes old
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 6 * 60 * 1000);
|
return FileTime.fromMillis(
|
||||||
} else {
|
System.currentTimeMillis() - 6 * 60 * 1000);
|
||||||
// Less than 5 minutes old
|
} else {
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 2 * 60 * 1000);
|
// Less than 5 minutes old
|
||||||
}
|
return FileTime.fromMillis(
|
||||||
});
|
System.currentTimeMillis() - 2 * 60 * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Configure Files.size to return 0 for empty files
|
// Configure Files.size to return 0 for empty files
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(0L);
|
||||||
.thenReturn(0L);
|
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
// For deleteIfExists, track which files would be deleted
|
||||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
deletedFiles.add(path);
|
invocation -> {
|
||||||
return true;
|
Path path = invocation.getArgument(0);
|
||||||
});
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertTrue(deletedFiles.contains(emptyFile),
|
assertTrue(
|
||||||
|
deletedFiles.contains(emptyFile),
|
||||||
"Empty file older than 5 minutes should be deleted");
|
"Empty file older than 5 minutes should be deleted");
|
||||||
assertFalse(deletedFiles.contains(recentEmptyFile),
|
assertFalse(
|
||||||
|
deletedFiles.contains(recentEmptyFile),
|
||||||
"Empty file newer than 5 minutes should not be deleted");
|
"Empty file newer than 5 minutes should not be deleted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,17 +413,13 @@ 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)) {
|
||||||
// Mock Files.list for each directory
|
// Mock Files.list for each directory
|
||||||
mockedFiles.when(() -> Files.list(eq(systemTempDir)))
|
mockedFiles.when(() -> Files.list(eq(systemTempDir))).thenReturn(Stream.of(dir1));
|
||||||
.thenReturn(Stream.of(dir1));
|
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir1)))
|
mockedFiles.when(() -> Files.list(eq(dir1))).thenReturn(Stream.of(tempFile1, dir2));
|
||||||
.thenReturn(Stream.of(tempFile1, dir2));
|
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir2)))
|
mockedFiles.when(() -> Files.list(eq(dir2))).thenReturn(Stream.of(tempFile2, dir3));
|
||||||
.thenReturn(Stream.of(tempFile2, dir3));
|
|
||||||
|
|
||||||
mockedFiles.when(() -> Files.list(eq(dir3)))
|
mockedFiles.when(() -> Files.list(eq(dir3))).thenReturn(Stream.of(tempFile3));
|
||||||
.thenReturn(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);
|
||||||
@ -394,31 +433,35 @@ public class TempFileCleanupServiceTest {
|
|||||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||||
|
|
||||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||||
mockedFiles.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
String fileName = path.getFileName().toString();
|
invocation -> {
|
||||||
|
Path path = invocation.getArgument(0);
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
|
||||||
if (fileName.contains("old")) {
|
if (fileName.contains("old")) {
|
||||||
// Old file
|
// Old file
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 5000000);
|
return FileTime.fromMillis(
|
||||||
} else {
|
System.currentTimeMillis() - 5000000);
|
||||||
// Recent file
|
} else {
|
||||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
// Recent file
|
||||||
}
|
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Configure Files.size to return normal size
|
// Configure Files.size to return normal size
|
||||||
mockedFiles.when(() -> Files.size(any(Path.class)))
|
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
||||||
.thenReturn(1024L);
|
|
||||||
|
|
||||||
// For deleteIfExists, track which files would be deleted
|
// For deleteIfExists, track which files would be deleted
|
||||||
mockedFiles.when(() -> Files.deleteIfExists(any(Path.class)))
|
mockedFiles
|
||||||
.thenAnswer(invocation -> {
|
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||||
Path path = invocation.getArgument(0);
|
.thenAnswer(
|
||||||
deletedFiles.add(path);
|
invocation -> {
|
||||||
return true;
|
Path path = invocation.getArgument(0);
|
||||||
});
|
deletedFiles.add(path);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||||
@ -430,14 +473,15 @@ public class TempFileCleanupServiceTest {
|
|||||||
// 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");
|
||||||
assertTrue(deletedFiles.contains(tempFile3), "Old temp file in nested directory should be deleted");
|
assertTrue(
|
||||||
|
deletedFiles.contains(tempFile3),
|
||||||
|
"Old temp file in nested directory should be deleted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Helper method to invoke the private cleanupDirectoryStreaming method using reflection */
|
||||||
* Helper method to invoke the private cleanupDirectoryStreaming method using reflection
|
private void invokeCleanupDirectoryStreaming(
|
||||||
*/
|
Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||||
private void invokeCleanupDirectoryStreaming(Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
try {
|
try {
|
||||||
// Create a consumer that tracks deleted files
|
// Create a consumer that tracks deleted files
|
||||||
@ -445,13 +489,26 @@ public class TempFileCleanupServiceTest {
|
|||||||
Consumer<Path> deleteCallback = path -> deleteCount.incrementAndGet();
|
Consumer<Path> deleteCallback = path -> deleteCount.incrementAndGet();
|
||||||
|
|
||||||
// Get the method with updated signature
|
// Get the method with updated signature
|
||||||
var method = TempFileCleanupService.class.getDeclaredMethod(
|
var method =
|
||||||
"cleanupDirectoryStreaming",
|
TempFileCleanupService.class.getDeclaredMethod(
|
||||||
Path.class, boolean.class, int.class, long.class, boolean.class, Consumer.class);
|
"cleanupDirectoryStreaming",
|
||||||
|
Path.class,
|
||||||
|
boolean.class,
|
||||||
|
int.class,
|
||||||
|
long.class,
|
||||||
|
boolean.class,
|
||||||
|
Consumer.class);
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
|
||||||
// Invoke the method with appropriate parameters
|
// Invoke the method with appropriate parameters
|
||||||
method.invoke(cleanupService, directory, containerMode, depth, maxAgeMillis, false, deleteCallback);
|
method.invoke(
|
||||||
|
cleanupService,
|
||||||
|
directory,
|
||||||
|
containerMode,
|
||||||
|
depth,
|
||||||
|
maxAgeMillis,
|
||||||
|
false,
|
||||||
|
deleteCallback);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Error invoking cleanupDirectoryStreaming", e);
|
throw new RuntimeException("Error invoking cleanupDirectoryStreaming", e);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,5 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.mockito.MockedStatic;
|
|
||||||
import org.mockito.Mockito;
|
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
@ -19,6 +10,18 @@ import static org.mockito.Mockito.times;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
class CheckProgramInstallTest {
|
class CheckProgramInstallTest {
|
||||||
|
|
||||||
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
||||||
|
@ -3,21 +3,46 @@ package stirling.software.common.util;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
import stirling.software.common.service.SsrfProtectionService;
|
||||||
|
|
||||||
class CustomHtmlSanitizerTest {
|
class CustomHtmlSanitizerTest {
|
||||||
|
|
||||||
|
private CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
|
||||||
|
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
|
||||||
|
mock(stirling.software.common.model.ApplicationProperties.class);
|
||||||
|
stirling.software.common.model.ApplicationProperties.System mockSystem =
|
||||||
|
mock(stirling.software.common.model.ApplicationProperties.System.class);
|
||||||
|
|
||||||
|
// Allow all URLs by default for basic tests
|
||||||
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
|
when(mockSystem.getDisableSanitize()).thenReturn(false); // Enable sanitization for tests
|
||||||
|
|
||||||
|
customHtmlSanitizer =
|
||||||
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource("provideHtmlTestCases")
|
@MethodSource("provideHtmlTestCases")
|
||||||
void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) {
|
void testSanitizeHtml(String inputHtml, String[] expectedContainedTags) {
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(inputHtml);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(inputHtml);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
for (String tag : expectedContainedTags) {
|
for (String tag : expectedContainedTags) {
|
||||||
@ -58,7 +83,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<p style=\"color: blue; font-size: 16px; margin-top: 10px;\">Styled text</p>";
|
"<p style=\"color: blue; font-size: 16px; margin-top: 10px;\">Styled text</p>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithStyles);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithStyles);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// The OWASP HTML Sanitizer might filter some specific styles, so we only check that
|
// The OWASP HTML Sanitizer might filter some specific styles, so we only check that
|
||||||
@ -75,7 +100,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<a href=\"https://example.com\" title=\"Example Site\">Example Link</a>";
|
"<a href=\"https://example.com\" title=\"Example Site\">Example Link</a>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithLink);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithLink);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// The most important aspect is that the link content is preserved
|
// The most important aspect is that the link content is preserved
|
||||||
@ -97,7 +122,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
String htmlWithJsLink = "<a href=\"javascript:alert('XSS')\">Malicious Link</a>";
|
String htmlWithJsLink = "<a href=\"javascript:alert('XSS')\">Malicious Link</a>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsLink);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithJsLink);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed");
|
assertFalse(sanitizedHtml.contains("javascript:"), "JavaScript URLs should be removed");
|
||||||
@ -116,7 +141,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
+ "</table>";
|
+ "</table>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithTable);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithTable);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
|
assertTrue(sanitizedHtml.contains("<table"), "Table should be preserved");
|
||||||
@ -143,7 +168,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithImage);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithImage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertTrue(sanitizedHtml.contains("<img"), "Image tag should be preserved");
|
assertTrue(sanitizedHtml.contains("<img"), "Image tag should be preserved");
|
||||||
@ -160,7 +185,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
|
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(
|
assertFalse(
|
||||||
@ -175,7 +200,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
"<a href=\"#\" onclick=\"alert('XSS')\" onmouseover=\"alert('XSS')\">Click me</a>";
|
"<a href=\"#\" onclick=\"alert('XSS')\" onmouseover=\"alert('XSS')\">Click me</a>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithJsEvent);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithJsEvent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(
|
assertFalse(
|
||||||
@ -192,7 +217,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
String htmlWithScript = "<p>Safe content</p><script>alert('XSS');</script>";
|
String htmlWithScript = "<p>Safe content</p><script>alert('XSS');</script>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithScript);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithScript);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("<script>"), "Script tags should be removed");
|
assertFalse(sanitizedHtml.contains("<script>"), "Script tags should be removed");
|
||||||
@ -206,7 +231,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
String htmlWithNoscript = "<p>Safe content</p><noscript>JavaScript is disabled</noscript>";
|
String htmlWithNoscript = "<p>Safe content</p><noscript>JavaScript is disabled</noscript>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithNoscript);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithNoscript);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("<noscript>"), "Noscript tags should be removed");
|
assertFalse(sanitizedHtml.contains("<noscript>"), "Noscript tags should be removed");
|
||||||
@ -220,7 +245,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
String htmlWithIframe = "<p>Safe content</p><iframe src=\"https://example.com\"></iframe>";
|
String htmlWithIframe = "<p>Safe content</p><iframe src=\"https://example.com\"></iframe>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithIframe);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithIframe);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tags should be removed");
|
assertFalse(sanitizedHtml.contains("<iframe"), "Iframe tags should be removed");
|
||||||
@ -237,7 +262,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
|
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithObjects);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("<object"), "Object tags should be removed");
|
assertFalse(sanitizedHtml.contains("<object"), "Object tags should be removed");
|
||||||
@ -256,7 +281,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
+ "<link rel=\"stylesheet\" href=\"evil.css\">";
|
+ "<link rel=\"stylesheet\" href=\"evil.css\">";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(htmlWithMetaTags);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithMetaTags);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertFalse(sanitizedHtml.contains("<meta"), "Meta tags should be removed");
|
assertFalse(sanitizedHtml.contains("<meta"), "Meta tags should be removed");
|
||||||
@ -283,7 +308,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
+ "</div>";
|
+ "</div>";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(complexHtml);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertTrue(sanitizedHtml.contains("<div"), "Div should be preserved");
|
assertTrue(sanitizedHtml.contains("<div"), "Div should be preserved");
|
||||||
@ -314,7 +339,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSanitizeHandlesEmpty() {
|
void testSanitizeHandlesEmpty() {
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize("");
|
String sanitizedHtml = customHtmlSanitizer.sanitize("");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertEquals("", sanitizedHtml, "Empty input should result in empty string");
|
assertEquals("", sanitizedHtml, "Empty input should result in empty string");
|
||||||
@ -323,7 +348,7 @@ class CustomHtmlSanitizerTest {
|
|||||||
@Test
|
@Test
|
||||||
void testSanitizeHandlesNull() {
|
void testSanitizeHandlesNull() {
|
||||||
// Act
|
// Act
|
||||||
String sanitizedHtml = CustomHtmlSanitizer.sanitize(null);
|
String sanitizedHtml = customHtmlSanitizer.sanitize(null);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@ import org.junit.jupiter.api.io.TempDir;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ -3,19 +3,40 @@ package stirling.software.common.util;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
|
import stirling.software.common.service.SsrfProtectionService;
|
||||||
|
|
||||||
public class FileToPdfTest {
|
public class FileToPdfTest {
|
||||||
|
|
||||||
|
private CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
|
||||||
|
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
|
||||||
|
mock(stirling.software.common.model.ApplicationProperties.class);
|
||||||
|
stirling.software.common.model.ApplicationProperties.System mockSystem =
|
||||||
|
mock(stirling.software.common.model.ApplicationProperties.System.class);
|
||||||
|
|
||||||
|
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||||
|
.thenReturn(true);
|
||||||
|
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||||
|
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
||||||
|
|
||||||
|
customHtmlSanitizer =
|
||||||
|
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
|
* Test the HTML to PDF conversion. This test expects an IOException when an empty HTML input is
|
||||||
* provided.
|
* provided.
|
||||||
@ -25,14 +46,13 @@ public class FileToPdfTest {
|
|||||||
HTMLToPdfRequest request = new HTMLToPdfRequest();
|
HTMLToPdfRequest request = new HTMLToPdfRequest();
|
||||||
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
|
byte[] fileBytes = new byte[0]; // Sample file bytes (empty input)
|
||||||
String fileName = "test.html"; // Sample file name indicating an HTML file
|
String fileName = "test.html"; // Sample file name indicating an HTML file
|
||||||
boolean disableSanitize = false; // Flag to control sanitization
|
|
||||||
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
|
TempFileManager tempFileManager = mock(TempFileManager.class); // Mock TempFileManager
|
||||||
|
|
||||||
// Mock the temp file creation to return real temp files
|
// Mock the temp file creation to return real temp files
|
||||||
try {
|
try {
|
||||||
when(tempFileManager.createTempFile(anyString()))
|
when(tempFileManager.createTempFile(anyString()))
|
||||||
.thenReturn(File.createTempFile("test", ".pdf"))
|
.thenReturn(Files.createTempFile("test", ".pdf").toFile())
|
||||||
.thenReturn(File.createTempFile("test", ".html"));
|
.thenReturn(Files.createTempFile("test", ".html").toFile());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
@ -43,7 +63,12 @@ public class FileToPdfTest {
|
|||||||
Exception.class,
|
Exception.class,
|
||||||
() ->
|
() ->
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
"/path/", request, fileBytes, fileName, disableSanitize, tempFileManager));
|
"/path/",
|
||||||
|
request,
|
||||||
|
fileBytes,
|
||||||
|
fileName,
|
||||||
|
tempFileManager,
|
||||||
|
customHtmlSanitizer));
|
||||||
assertNotNull(thrown);
|
assertNotNull(thrown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import org.junit.jupiter.api.Assertions;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.Arguments;
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
import org.junit.jupiter.params.provider.MethodSource;
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import stirling.software.common.model.enumeration.UsernameAttribute;
|
import stirling.software.common.model.enumeration.UsernameAttribute;
|
||||||
import stirling.software.common.model.oauth2.GitHubProvider;
|
import stirling.software.common.model.oauth2.GitHubProvider;
|
||||||
import stirling.software.common.model.oauth2.GoogleProvider;
|
import stirling.software.common.model.oauth2.GoogleProvider;
|
||||||
import stirling.software.common.model.oauth2.Provider;
|
import stirling.software.common.model.oauth2.Provider;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ProviderUtilsTest {
|
class ProviderUtilsTest {
|
||||||
@ -40,7 +42,7 @@ class ProviderUtilsTest {
|
|||||||
public static Stream<Arguments> providerParams() {
|
public static Stream<Arguments> providerParams() {
|
||||||
Provider generic = null;
|
Provider generic = null;
|
||||||
var google =
|
var google =
|
||||||
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
|
new GoogleProvider(null, "clientSecret", List.of("scope"), UsernameAttribute.EMAIL);
|
||||||
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
|
var github = new GitHubProvider("clientId", "", List.of("scope"), UsernameAttribute.LOGIN);
|
||||||
|
|
||||||
return Stream.of(Arguments.of(generic), Arguments.of(google), Arguments.of(github));
|
return Stream.of(Arguments.of(generic), Arguments.of(google), Arguments.of(github));
|
||||||
|
@ -42,7 +42,6 @@ class SpringContextHolderTest {
|
|||||||
verify(mockApplicationContext).getBean(TestBean.class);
|
verify(mockApplicationContext).getBean(TestBean.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testGetBean_ApplicationContextNotSet() {
|
void testGetBean_ApplicationContextNotSet() {
|
||||||
// Don't set application context
|
// Don't set application context
|
||||||
@ -58,7 +57,8 @@ class SpringContextHolderTest {
|
|||||||
void testGetBean_BeanNotFound() {
|
void testGetBean_BeanNotFound() {
|
||||||
// Arrange
|
// Arrange
|
||||||
contextHolder.setApplicationContext(mockApplicationContext);
|
contextHolder.setApplicationContext(mockApplicationContext);
|
||||||
when(mockApplicationContext.getBean(TestBean.class)).thenThrow(new org.springframework.beans.BeansException("Bean not found") {});
|
when(mockApplicationContext.getBean(TestBean.class))
|
||||||
|
.thenThrow(new org.springframework.beans.BeansException("Bean not found") {});
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
TestBean result = SpringContextHolder.getBean(TestBean.class);
|
||||||
@ -68,6 +68,5 @@ class SpringContextHolderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Simple test class
|
// Simple test class
|
||||||
private static class TestBean {
|
private static class TestBean {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
package stirling.software.common.util.misc;
|
package stirling.software.common.util.misc;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.misc.HighContrastColorCombination;
|
||||||
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
|
||||||
class HighContrastColorReplaceDeciderTest {
|
class HighContrastColorReplaceDeciderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
|
||||||
class InvertFullColorStrategyTest {
|
class InvertFullColorStrategyTest {
|
||||||
|
@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
import stirling.software.common.model.api.misc.ReplaceAndInvert;
|
||||||
|
|
||||||
class ReplaceAndInvertColorStrategyTest {
|
class ReplaceAndInvertColorStrategyTest {
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
package stirling.software.common.util.propertyeditor;
|
package stirling.software.common.util.propertyeditor;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import stirling.software.common.model.api.security.RedactionArea;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.security.RedactionArea;
|
||||||
|
|
||||||
class StringToArrayListPropertyEditorTest {
|
class StringToArrayListPropertyEditorTest {
|
||||||
|
|
||||||
private StringToArrayListPropertyEditor editor;
|
private StringToArrayListPropertyEditor editor;
|
||||||
|
1
app/common/src/test/resources/saml/dummy.txt
Normal file
1
app/common/src/test/resources/saml/dummy.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
ok
|
3
app/core/.gitignore
vendored
3
app/core/.gitignore
vendored
@ -16,8 +16,7 @@ local.properties
|
|||||||
version.properties
|
version.properties
|
||||||
|
|
||||||
#### Stirling-PDF Files ###
|
#### Stirling-PDF Files ###
|
||||||
pipeline/watchedFolders/
|
pipeline/*
|
||||||
pipeline/finishedFolders/
|
|
||||||
customFiles/
|
customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
@ -14,7 +14,7 @@ configurations {
|
|||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
target sourceSets.main.allJava
|
target 'src/**/java/**/*.java'
|
||||||
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
googleJavaFormat(googleJavaFormatVersion).aosp().reorderImports(false)
|
||||||
|
|
||||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
||||||
@ -23,6 +23,18 @@ spotless {
|
|||||||
leadingTabsToSpaces()
|
leadingTabsToSpaces()
|
||||||
endWithNewline()
|
endWithNewline()
|
||||||
}
|
}
|
||||||
|
yaml {
|
||||||
|
target '**/*.yml', '**/*.yaml'
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
leadingTabsToSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
format 'gradle', {
|
||||||
|
target '**/gradle/*.gradle', '**/*.gradle'
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
leadingTabsToSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -43,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.19.0'
|
implementation 'commons-io:commons-io:2.20.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.2'
|
implementation 'io.micrometer:micrometer-core:1.15.2'
|
||||||
@ -62,7 +74,7 @@ dependencies {
|
|||||||
exclude group: 'com.google.code.gson', module: 'gson'
|
exclude group: 'com.google.code.gson', module: 'gson'
|
||||||
}
|
}
|
||||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
implementation 'com.opencsv:opencsv:5.11.2' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
|
|
||||||
// Batik
|
// Batik
|
||||||
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
implementation 'org.apache.xmlgraphics:batik-all:1.19'
|
||||||
@ -79,7 +91,7 @@ dependencies {
|
|||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
||||||
|
@ -36,13 +36,15 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
String requestURI = request.getRequestURI();
|
||||||
|
|
||||||
|
// Skip URL cleaning for API endpoints - they need their own parameter handling
|
||||||
|
if (requestURI.contains("/api/")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
|
||||||
|
|
||||||
if (requestURI.contains("/api/")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, String> allowedParameters = new HashMap<>();
|
Map<String, String> allowedParameters = new HashMap<>();
|
||||||
|
|
||||||
|
@ -421,7 +421,6 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
// file-to-pdf has multiple implementations
|
// file-to-pdf has multiple implementations
|
||||||
addEndpointAlternative("file-to-pdf", "LibreOffice");
|
addEndpointAlternative("file-to-pdf", "LibreOffice");
|
||||||
addEndpointAlternative("file-to-pdf", "Python");
|
|
||||||
addEndpointAlternative("file-to-pdf", "Unoconvert");
|
addEndpointAlternative("file-to-pdf", "Unoconvert");
|
||||||
|
|
||||||
// pdf-to-html and pdf-to-markdown can use either LibreOffice or Pdftohtml
|
// pdf-to-html and pdf-to-markdown can use either LibreOffice or Pdftohtml
|
||||||
|
@ -35,6 +35,7 @@ public class InitialSetup {
|
|||||||
initEnableCSRFSecurity();
|
initEnableCSRFSecurity();
|
||||||
initLegalUrls();
|
initLegalUrls();
|
||||||
initSetAppVersion();
|
initSetAppVersion();
|
||||||
|
GeneralUtils.extractPipeline();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initUUIDKey() throws IOException {
|
public void initUUIDKey() throws IOException {
|
||||||
|
@ -25,6 +25,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||||
import stirling.software.common.util.EmlToPdf;
|
import stirling.software.common.util.EmlToPdf;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@ -39,6 +40,7 @@ public class ConvertEmlToPDF {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/eml/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -105,9 +107,9 @@ public class ConvertEmlToPDF {
|
|||||||
request,
|
request,
|
||||||
fileBytes,
|
fileBytes,
|
||||||
originalFilename,
|
originalFilename,
|
||||||
false,
|
|
||||||
pdfDocumentFactory,
|
pdfDocumentFactory,
|
||||||
tempFileManager);
|
tempFileManager,
|
||||||
|
customHtmlSanitizer);
|
||||||
|
|
||||||
if (pdfBytes == null || pdfBytes.length == 0) {
|
if (pdfBytes == null || pdfBytes.length == 0) {
|
||||||
log.error("PDF conversion failed - empty output for {}", originalFilename);
|
log.error("PDF conversion failed - empty output for {}", originalFilename);
|
||||||
|
@ -16,9 +16,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
|
||||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.FileToPdf;
|
import stirling.software.common.util.FileToPdf;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
@ -32,13 +32,14 @@ public class ConvertHtmlToPDF {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
|
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
description =
|
description =
|
||||||
@ -59,17 +60,14 @@ public class ConvertHtmlToPDF {
|
|||||||
"error.fileFormatRequired", "File must be in {0} format", ".html or .zip");
|
"error.fileFormatRequired", "File must be in {0} format", ".html or .zip");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean disableSanitize =
|
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
runtimePathConfig.getWeasyPrintPath(),
|
runtimePathConfig.getWeasyPrintPath(),
|
||||||
request,
|
request,
|
||||||
fileInput.getBytes(),
|
fileInput.getBytes(),
|
||||||
originalFilename,
|
originalFilename,
|
||||||
disableSanitize,
|
tempFileManager,
|
||||||
tempFileManager);
|
customHtmlSanitizer);
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.ApplicationProperties;
|
|
||||||
import stirling.software.common.model.api.GeneralFile;
|
import stirling.software.common.model.api.GeneralFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
import stirling.software.common.util.FileToPdf;
|
import stirling.software.common.util.FileToPdf;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
@ -41,13 +41,14 @@ import stirling.software.common.util.WebResponseUtils;
|
|||||||
public class ConvertMarkdownToPdf {
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
|
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a Markdown file to PDF",
|
summary = "Convert a Markdown file to PDF",
|
||||||
description =
|
description =
|
||||||
@ -81,17 +82,14 @@ public class ConvertMarkdownToPdf {
|
|||||||
|
|
||||||
String htmlContent = renderer.render(document);
|
String htmlContent = renderer.render(document);
|
||||||
|
|
||||||
boolean disableSanitize =
|
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
runtimePathConfig.getWeasyPrintPath(),
|
runtimePathConfig.getWeasyPrintPath(),
|
||||||
null,
|
null,
|
||||||
htmlContent.getBytes(),
|
htmlContent.getBytes(),
|
||||||
"converted.html",
|
"converted.html",
|
||||||
disableSanitize,
|
tempFileManager,
|
||||||
tempFileManager);
|
customHtmlSanitizer);
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
@ -4,6 +4,7 @@ import stirling.software.common.annotations.AutoJobPostMapping;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -28,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import stirling.software.common.configuration.RuntimePathConfig;
|
import stirling.software.common.configuration.RuntimePathConfig;
|
||||||
import stirling.software.common.model.api.GeneralFile;
|
import stirling.software.common.model.api.GeneralFile;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||||
import stirling.software.common.util.ProcessExecutor;
|
import stirling.software.common.util.ProcessExecutor;
|
||||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
@ -40,6 +42,7 @@ public class ConvertOfficeController {
|
|||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||||
|
|
||||||
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||||
// Check for valid file extension
|
// Check for valid file extension
|
||||||
@ -52,7 +55,17 @@ public class ConvertOfficeController {
|
|||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile =
|
Path tempInputFile =
|
||||||
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
||||||
inputFile.transferTo(tempInputFile);
|
|
||||||
|
// Check if the file is HTML and apply sanitization if needed
|
||||||
|
String fileExtension = FilenameUtils.getExtension(originalFilename).toLowerCase();
|
||||||
|
if ("html".equals(fileExtension) || "htm".equals(fileExtension)) {
|
||||||
|
// Read and sanitize HTML content
|
||||||
|
String htmlContent = new String(inputFile.getBytes(), StandardCharsets.UTF_8);
|
||||||
|
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlContent);
|
||||||
|
Files.write(tempInputFile, sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} else {
|
||||||
|
inputFile.transferTo(tempInputFile);
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
@ -72,9 +72,17 @@ public class StampController {
|
|||||||
String stampType = request.getStampType();
|
String stampType = request.getStampType();
|
||||||
String stampText = request.getStampText();
|
String stampText = request.getStampText();
|
||||||
MultipartFile stampImage = request.getStampImage();
|
MultipartFile stampImage = request.getStampImage();
|
||||||
String stampImageName = stampImage.getOriginalFilename();
|
if ("image".equalsIgnoreCase(stampType)) {
|
||||||
if (stampImageName.contains("..") || stampImageName.startsWith("/")) {
|
if (stampImage == null) {
|
||||||
throw new IllegalArgumentException("Invalid stamp image file path");
|
throw new IllegalArgumentException(
|
||||||
|
"Stamp image file must be provided when stamp type is 'image'");
|
||||||
|
}
|
||||||
|
String stampImageName = stampImage.getOriginalFilename();
|
||||||
|
if (stampImageName == null
|
||||||
|
|| stampImageName.contains("..")
|
||||||
|
|| stampImageName.startsWith("/")) {
|
||||||
|
throw new IllegalArgumentException("Invalid stamp image file path");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
String alphabet = request.getAlphabet();
|
String alphabet = request.getAlphabet();
|
||||||
float fontSize = request.getFontSize();
|
float fontSize = request.getFontSize();
|
||||||
|
@ -108,9 +108,13 @@ public class PipelineProcessor {
|
|||||||
if (inputFileTypes == null) {
|
if (inputFileTypes == null) {
|
||||||
inputFileTypes = new ArrayList<String>(Arrays.asList("ALL"));
|
inputFileTypes = new ArrayList<String>(Arrays.asList("ALL"));
|
||||||
}
|
}
|
||||||
if (!operation.matches("^[a-zA-Z0-9_-]+$")) {
|
|
||||||
throw new IllegalArgumentException("Invalid operation value received.");
|
if (!apiDocService.isValidOperation(operation, parameters)) {
|
||||||
|
log.error("Invalid operation or parameters: o:{} p:{}", operation, parameters);
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid operation: " + operation + " with parameters: " + parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
String url = getBaseUrl() + operation;
|
String url = getBaseUrl() + operation;
|
||||||
List<Resource> newOutputFiles = new ArrayList<>();
|
List<Resource> newOutputFiles = new ArrayList<>();
|
||||||
if (!isMultiInputOperation) {
|
if (!isMultiInputOperation) {
|
||||||
@ -136,7 +140,7 @@ public class PipelineProcessor {
|
|||||||
// skip
|
// skip
|
||||||
// this
|
// this
|
||||||
// file
|
// file
|
||||||
if (operation.startsWith("filter-")
|
if (operation.startsWith("/api/v1/filter/filter-")
|
||||||
&& (response.getBody() == null
|
&& (response.getBody() == null
|
||||||
|| response.getBody().length == 0)) {
|
|| response.getBody().length == 0)) {
|
||||||
filtersApplied = true;
|
filtersApplied = true;
|
||||||
|
@ -8,6 +8,8 @@ import org.springframework.web.servlet.ModelAndView;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.common.model.ApplicationProperties;
|
||||||
|
import stirling.software.common.util.ApplicationContextProvider;
|
||||||
import stirling.software.common.util.CheckProgramInstall;
|
import stirling.software.common.util.CheckProgramInstall;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@ -62,6 +64,13 @@ public class ConverterWebController {
|
|||||||
@Hidden
|
@Hidden
|
||||||
public String pdfToimgForm(Model model) {
|
public String pdfToimgForm(Model model) {
|
||||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||||
|
ApplicationProperties properties =
|
||||||
|
ApplicationContextProvider.getBean(ApplicationProperties.class);
|
||||||
|
if (properties != null && properties.getSystem() != null) {
|
||||||
|
model.addAttribute("maxDPI", properties.getSystem().getMaxDPI());
|
||||||
|
} else {
|
||||||
|
model.addAttribute("maxDPI", 500); // Default value if not set
|
||||||
|
}
|
||||||
model.addAttribute("isPython", isPython);
|
model.addAttribute("isPython", isPython);
|
||||||
model.addAttribute("currentPage", "pdf-to-img");
|
model.addAttribute("currentPage", "pdf-to-img");
|
||||||
return "convert/pdf-to-img";
|
return "convert/pdf-to-img";
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=المفضل
|
|||||||
settings.title=الإعدادات
|
settings.title=الإعدادات
|
||||||
settings.update=التحديث متاح
|
settings.update=التحديث متاح
|
||||||
settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح.
|
settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=إصدار التطبيق:
|
settings.appVersion=إصدار التطبيق:
|
||||||
settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط):
|
settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط):
|
||||||
settings.downloadOption.1=فتح في نفس النافذة
|
settings.downloadOption.1=فتح في نفس النافذة
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=نوع اللون
|
|||||||
pdfToImage.color=اللون
|
pdfToImage.color=اللون
|
||||||
pdfToImage.grey=تدرج الرمادي
|
pdfToImage.grey=تدرج الرمادي
|
||||||
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
|
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=تحويل
|
pdfToImage.submit=تحويل
|
||||||
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
|
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
|
||||||
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
|
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Populyar
|
|||||||
settings.title=Parametrlər
|
settings.title=Parametrlər
|
||||||
settings.update=Yeniləmə mövcuddur
|
settings.update=Yeniləmə mövcuddur
|
||||||
settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur.
|
settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Proqram Versiyası:
|
settings.appVersion=Proqram Versiyası:
|
||||||
settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün):
|
settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün):
|
||||||
settings.downloadOption.1=Eyni pəncərədə açın
|
settings.downloadOption.1=Eyni pəncərədə açın
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Rəng Tipi
|
|||||||
pdfToImage.color=Rəng
|
pdfToImage.color=Rəng
|
||||||
pdfToImage.grey=Boz Tonlama
|
pdfToImage.grey=Boz Tonlama
|
||||||
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
|
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Çevir
|
pdfToImage.submit=Çevir
|
||||||
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
|
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
|
||||||
pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)
|
pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Популярни
|
|||||||
settings.title=Настройки
|
settings.title=Настройки
|
||||||
settings.update=Налична актуализация
|
settings.update=Налична актуализация
|
||||||
settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}).
|
settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}).
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Версия на приложението:
|
settings.appVersion=Версия на приложението:
|
||||||
settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран):
|
settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран):
|
||||||
settings.downloadOption.1=Отваряне в същия прозорец
|
settings.downloadOption.1=Отваряне в същия прозорец
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Тип цвят
|
|||||||
pdfToImage.color=Цвят
|
pdfToImage.color=Цвят
|
||||||
pdfToImage.grey=Скала на сивото
|
pdfToImage.grey=Скала на сивото
|
||||||
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
|
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Преобразуване
|
pdfToImage.submit=Преобразуване
|
||||||
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
|
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
|
||||||
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=སྤྱི་མོས།
|
|||||||
settings.title=སྒྲིག་འགོད།
|
settings.title=སྒྲིག་འགོད།
|
||||||
settings.update=གསར་སྒྱུར་ཡོད།
|
settings.update=གསར་སྒྱུར་ཡོད།
|
||||||
settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད།
|
settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད།
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=མཉེན་ཆས་པར་གཞི།
|
settings.appVersion=མཉེན་ཆས་པར་གཞི།
|
||||||
settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།):
|
settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།):
|
||||||
settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ།
|
settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ།
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=ཚོས་མདོག་གི་རིགས།
|
|||||||
pdfToImage.color=ཚོས་མདོག
|
pdfToImage.color=ཚོས་མདོག
|
||||||
pdfToImage.grey=སྐྱ་མདོག
|
pdfToImage.grey=སྐྱ་མདོག
|
||||||
pdfToImage.blackwhite=དཀར་ནག (གནས་ཚུལ་བརླག་སྲིད།)
|
pdfToImage.blackwhite=དཀར་ནག (གནས་ཚུལ་བརླག་སྲིད།)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=བསྒྱུར་བ།
|
pdfToImage.submit=བསྒྱུར་བ།
|
||||||
pdfToImage.info=Python སྒྲིག་འཇུག་བྱས་མི་འདུག WebP བསྒྱུར་བར་དགོས་མཁོ་ཡིན།
|
pdfToImage.info=Python སྒྲིག་འཇུག་བྱས་མི་འདུག WebP བསྒྱུར་བར་དགོས་མཁོ་ཡིན།
|
||||||
pdfToImage.placeholder=(དཔེར་ན། 1,2,8 ཡང་ན་ 4,7,12-16 ཡང་ན་ 2n-1)
|
pdfToImage.placeholder=(དཔེར་ན། 1,2,8 ཡང་ན་ 4,7,12-16 ཡང་ན་ 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popular
|
|||||||
settings.title=Opcions
|
settings.title=Opcions
|
||||||
settings.update=Actualització Disponible
|
settings.update=Actualització Disponible
|
||||||
settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible.
|
settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Versió de l'App:
|
settings.appVersion=Versió de l'App:
|
||||||
settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit):
|
settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit):
|
||||||
settings.downloadOption.1=Obre en la mateixa finestra
|
settings.downloadOption.1=Obre en la mateixa finestra
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Tipus de Color
|
|||||||
pdfToImage.color=Color
|
pdfToImage.color=Color
|
||||||
pdfToImage.grey=Escala de Grisos
|
pdfToImage.grey=Escala de Grisos
|
||||||
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
|
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Converteix
|
pdfToImage.submit=Converteix
|
||||||
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
|
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
|
||||||
pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)
|
pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Oblíbené
|
|||||||
settings.title=Nastavení
|
settings.title=Nastavení
|
||||||
settings.update=K dispozici je aktualizace
|
settings.update=K dispozici je aktualizace
|
||||||
settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}).
|
settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}).
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Verze aplikace:
|
settings.appVersion=Verze aplikace:
|
||||||
settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu):
|
settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu):
|
||||||
settings.downloadOption.1=Otevřít ve stejném okně
|
settings.downloadOption.1=Otevřít ve stejném okně
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Typ barev
|
|||||||
pdfToImage.color=Barevný
|
pdfToImage.color=Barevný
|
||||||
pdfToImage.grey=Stupně šedi
|
pdfToImage.grey=Stupně šedi
|
||||||
pdfToImage.blackwhite=Černobílý (Může dojít ke ztrátě dat!)
|
pdfToImage.blackwhite=Černobílý (Může dojít ke ztrátě dat!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Převést
|
pdfToImage.submit=Převést
|
||||||
pdfToImage.info=Python není nainstalován. Vyžadován pro konverzi do WebP.
|
pdfToImage.info=Python není nainstalován. Vyžadován pro konverzi do WebP.
|
||||||
pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)
|
pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Populære
|
|||||||
settings.title=Indstillinger
|
settings.title=Indstillinger
|
||||||
settings.update=Opdatering tilgængelig
|
settings.update=Opdatering tilgængelig
|
||||||
settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig.
|
settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=App Version:
|
settings.appVersion=App Version:
|
||||||
settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads):
|
settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads):
|
||||||
settings.downloadOption.1=Åbn i samme vindue
|
settings.downloadOption.1=Åbn i samme vindue
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Farvetype
|
|||||||
pdfToImage.color=Farve
|
pdfToImage.color=Farve
|
||||||
pdfToImage.grey=Gråtone
|
pdfToImage.grey=Gråtone
|
||||||
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
|
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Konvertér
|
pdfToImage.submit=Konvertér
|
||||||
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
|
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
|
||||||
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
|
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Beliebt
|
|||||||
settings.title=Einstellungen
|
settings.title=Einstellungen
|
||||||
settings.update=Update verfügbar
|
settings.update=Update verfügbar
|
||||||
settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar.
|
settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=App-Version:
|
settings.appVersion=App-Version:
|
||||||
settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind):
|
settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind):
|
||||||
settings.downloadOption.1=Im selben Fenster öffnen
|
settings.downloadOption.1=Im selben Fenster öffnen
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Farbtyp
|
|||||||
pdfToImage.color=Farbe
|
pdfToImage.color=Farbe
|
||||||
pdfToImage.grey=Graustufen
|
pdfToImage.grey=Graustufen
|
||||||
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
|
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Umwandeln
|
pdfToImage.submit=Umwandeln
|
||||||
pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung.
|
pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung.
|
||||||
pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1)
|
pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Vorhandene Lesezeichen ersetzen (deaktiviere
|
|||||||
editTableOfContents.editorTitle=Lesezeichen-Editor
|
editTableOfContents.editorTitle=Lesezeichen-Editor
|
||||||
editTableOfContents.editorDesc=Fügen unten Lesezeichen hinzu und ordne sie an. Klicke auf +, um das untergeordnete Lesezeichen hinzuzufügen.
|
editTableOfContents.editorDesc=Fügen unten Lesezeichen hinzu und ordne sie an. Klicke auf +, um das untergeordnete Lesezeichen hinzuzufügen.
|
||||||
editTableOfContents.addBookmark=Neues Lesezeichen hinzufügen
|
editTableOfContents.addBookmark=Neues Lesezeichen hinzufügen
|
||||||
|
editTableOfContents.importBookmarksDefault=Importieren
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=JSON-Datei hochladen
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Aus Zwischenablage einfügen
|
||||||
|
editTableOfContents.exportBookmarksDefault=Exportieren
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Als JSON herunterladen
|
||||||
|
editTableOfContents.exportBookmarksAsText=Als Text kopieren
|
||||||
editTableOfContents.desc.1=Mit diesem Werkzeug können Sie das Inhaltsverzeichnis (Lesezeichen) eines PDF-Dokuments hinzufügen oder bearbeiten.
|
editTableOfContents.desc.1=Mit diesem Werkzeug können Sie das Inhaltsverzeichnis (Lesezeichen) eines PDF-Dokuments hinzufügen oder bearbeiten.
|
||||||
editTableOfContents.desc.2=Sie können eine hierarchische Struktur erstellen, indem Sie untergeordnete Lesezeichen zu übergeordneten hinzufügen.
|
editTableOfContents.desc.2=Sie können eine hierarchische Struktur erstellen, indem Sie untergeordnete Lesezeichen zu übergeordneten hinzufügen.
|
||||||
editTableOfContents.desc.3=Jedes Lesezeichen benötigt einen Titel und eine Seitenzahl.
|
editTableOfContents.desc.3=Jedes Lesezeichen benötigt einen Titel und eine Seitenzahl.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Δημοφιλή
|
|||||||
settings.title=Ρυθμίσεις
|
settings.title=Ρυθμίσεις
|
||||||
settings.update=Διαθέσιμη ενημέρωση
|
settings.update=Διαθέσιμη ενημέρωση
|
||||||
settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη.
|
settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Έκδοση εφαρμογής:
|
settings.appVersion=Έκδοση εφαρμογής:
|
||||||
settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση):
|
settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση):
|
||||||
settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο
|
settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Τύπος χρώματος
|
|||||||
pdfToImage.color=Έγχρωμο
|
pdfToImage.color=Έγχρωμο
|
||||||
pdfToImage.grey=Κλίμακα του γκρι
|
pdfToImage.grey=Κλίμακα του γκρι
|
||||||
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
|
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Μετατροπή
|
pdfToImage.submit=Μετατροπή
|
||||||
pdfToImage.info=Η Python δεν είναι εγκατεστημένη. Απαιτείται για μετατροπή WebP.
|
pdfToImage.info=Η Python δεν είναι εγκατεστημένη. Απαιτείται για μετατροπή WebP.
|
||||||
pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)
|
pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popular
|
|||||||
settings.title=Settings
|
settings.title=Settings
|
||||||
settings.update=Update available
|
settings.update=Update available
|
||||||
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=App Version:
|
settings.appVersion=App Version:
|
||||||
settings.downloadOption.title=Choose download option (For single file non zip downloads):
|
settings.downloadOption.title=Choose download option (For single file non zip downloads):
|
||||||
settings.downloadOption.1=Open in same window
|
settings.downloadOption.1=Open in same window
|
||||||
@ -569,12 +601,12 @@ rotate.tags=server side
|
|||||||
|
|
||||||
|
|
||||||
home.imageToPdf.title=Image to PDF
|
home.imageToPdf.title=Image to PDF
|
||||||
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
|
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
|
||||||
imageToPdf.tags=conversion,img,jpg,picture,photo
|
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfToImage.title=PDF to Image
|
home.pdfToImage.title=PDF to Image
|
||||||
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
|
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
|
||||||
pdfToImage.tags=conversion,img,jpg,picture,photo
|
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfOrganiser.title=Organise
|
home.pdfOrganiser.title=Organise
|
||||||
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
|
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Colour type
|
|||||||
pdfToImage.color=Colour
|
pdfToImage.color=Colour
|
||||||
pdfToImage.grey=Greyscale
|
pdfToImage.grey=Greyscale
|
||||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Convert
|
pdfToImage.submit=Convert
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
@ -1843,6 +1876,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popular
|
|||||||
settings.title=Settings
|
settings.title=Settings
|
||||||
settings.update=Update available
|
settings.update=Update available
|
||||||
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=App Version:
|
settings.appVersion=App Version:
|
||||||
settings.downloadOption.title=Choose download option (For single file non zip downloads):
|
settings.downloadOption.title=Choose download option (For single file non zip downloads):
|
||||||
settings.downloadOption.1=Open in same window
|
settings.downloadOption.1=Open in same window
|
||||||
@ -569,12 +601,12 @@ rotate.tags=server side
|
|||||||
|
|
||||||
|
|
||||||
home.imageToPdf.title=Image to PDF
|
home.imageToPdf.title=Image to PDF
|
||||||
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF.
|
home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF.
|
||||||
imageToPdf.tags=conversion,img,jpg,picture,photo
|
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfToImage.title=PDF to Image
|
home.pdfToImage.title=PDF to Image
|
||||||
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF)
|
home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD)
|
||||||
pdfToImage.tags=conversion,img,jpg,picture,photo
|
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfOrganiser.title=Organize
|
home.pdfOrganiser.title=Organize
|
||||||
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
|
home.pdfOrganiser.desc=Remove/Rearrange pages in any order
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Color type
|
|||||||
pdfToImage.color=Color
|
pdfToImage.color=Color
|
||||||
pdfToImage.grey=Grayscale
|
pdfToImage.grey=Grayscale
|
||||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Convert
|
pdfToImage.submit=Convert
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Populares
|
|||||||
settings.title=Configuración
|
settings.title=Configuración
|
||||||
settings.update=Actualización disponible
|
settings.update=Actualización disponible
|
||||||
settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}).
|
settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}).
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Versión de la aplicación:
|
settings.appVersion=Versión de la aplicación:
|
||||||
settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP):
|
settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP):
|
||||||
settings.downloadOption.1=Abrir en la misma ventana
|
settings.downloadOption.1=Abrir en la misma ventana
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Tipo de color
|
|||||||
pdfToImage.color=Color
|
pdfToImage.color=Color
|
||||||
pdfToImage.grey=Escala de grises
|
pdfToImage.grey=Escala de grises
|
||||||
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
|
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Convertir
|
pdfToImage.submit=Convertir
|
||||||
pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP.
|
pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP.
|
||||||
pdfToImage.placeholder=(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1)
|
pdfToImage.placeholder=(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popular
|
|||||||
settings.title=Ezarpenak
|
settings.title=Ezarpenak
|
||||||
settings.update=Eguneratze eskuragarria
|
settings.update=Eguneratze eskuragarria
|
||||||
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
settings.updateAvailable={0} is the current installed version. A new version ({1}) is available.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Aplikazioaren bertsioa:
|
settings.appVersion=Aplikazioaren bertsioa:
|
||||||
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
|
settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe):
|
||||||
settings.downloadOption.1=Ireki leiho berean
|
settings.downloadOption.1=Ireki leiho berean
|
||||||
@ -570,11 +602,11 @@ rotate.tags=server side
|
|||||||
|
|
||||||
home.imageToPdf.title=Irudia PDF bihurtu
|
home.imageToPdf.title=Irudia PDF bihurtu
|
||||||
home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu
|
home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu
|
||||||
imageToPdf.tags=conversion,img,jpg,picture,photo
|
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfToImage.title=PDFa irudi bihurtu
|
home.pdfToImage.title=PDFa irudi bihurtu
|
||||||
home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu
|
home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu
|
||||||
pdfToImage.tags=conversion,img,jpg,picture,photo
|
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfOrganiser.title=Antolatzailea
|
home.pdfOrganiser.title=Antolatzailea
|
||||||
home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan
|
home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Kolore-mota
|
|||||||
pdfToImage.color=Kolorea
|
pdfToImage.color=Kolorea
|
||||||
pdfToImage.grey=Gris-eskala
|
pdfToImage.grey=Gris-eskala
|
||||||
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
|
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Bihurtu
|
pdfToImage.submit=Bihurtu
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=محبوب
|
|||||||
settings.title=تنظیمات
|
settings.title=تنظیمات
|
||||||
settings.update=بهروزرسانی موجود است
|
settings.update=بهروزرسانی موجود است
|
||||||
settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است.
|
settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=نسخه برنامه:
|
settings.appVersion=نسخه برنامه:
|
||||||
settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده):
|
settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده):
|
||||||
settings.downloadOption.1=باز کردن در همان پنجره
|
settings.downloadOption.1=باز کردن در همان پنجره
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=نوع رنگ
|
|||||||
pdfToImage.color=رنگ
|
pdfToImage.color=رنگ
|
||||||
pdfToImage.grey=خاکستری
|
pdfToImage.grey=خاکستری
|
||||||
pdfToImage.blackwhite=سیاه و سفید (ممکن است اطلاعات از دست برود!)
|
pdfToImage.blackwhite=سیاه و سفید (ممکن است اطلاعات از دست برود!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=تبدیل
|
pdfToImage.submit=تبدیل
|
||||||
pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است.
|
pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است.
|
||||||
pdfToImage.placeholder=(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1)
|
pdfToImage.placeholder=(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Populaire
|
|||||||
settings.title=Paramètres
|
settings.title=Paramètres
|
||||||
settings.update=Mise à jour disponible
|
settings.update=Mise à jour disponible
|
||||||
settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible.
|
settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Version de l'application :
|
settings.appVersion=Version de l'application :
|
||||||
settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) :
|
settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) :
|
||||||
settings.downloadOption.1=Ouvrir dans la même fenêtre
|
settings.downloadOption.1=Ouvrir dans la même fenêtre
|
||||||
@ -569,7 +601,7 @@ rotate.tags=pivoter,server side,rotate
|
|||||||
|
|
||||||
|
|
||||||
home.imageToPdf.title=Image en PDF
|
home.imageToPdf.title=Image en PDF
|
||||||
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF) en PDF.
|
home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF.
|
||||||
imageToPdf.tags=pdf,conversion,img,jpg,image,photo
|
imageToPdf.tags=pdf,conversion,img,jpg,image,photo
|
||||||
|
|
||||||
home.pdfToImage.title=PDF en image
|
home.pdfToImage.title=PDF en image
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Type d'impression
|
|||||||
pdfToImage.color=Couleur
|
pdfToImage.color=Couleur
|
||||||
pdfToImage.grey=Niveaux de gris
|
pdfToImage.grey=Niveaux de gris
|
||||||
pdfToImage.blackwhite=Noir et blanc (peut engendrer une perte de données !)
|
pdfToImage.blackwhite=Noir et blanc (peut engendrer une perte de données !)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Convertir
|
pdfToImage.submit=Convertir
|
||||||
pdfToImage.info=Python n'est pas installé. Nécessaire pour la conversion WebP.
|
pdfToImage.info=Python n'est pas installé. Nécessaire pour la conversion WebP.
|
||||||
pdfToImage.placeholder=(par exemple : 1,2,8 ou 4,7,12-16 ou 2n-1)
|
pdfToImage.placeholder=(par exemple : 1,2,8 ou 4,7,12-16 ou 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Remplacer les signets existants (décocher p
|
|||||||
editTableOfContents.editorTitle=Éditeur de signets
|
editTableOfContents.editorTitle=Éditeur de signets
|
||||||
editTableOfContents.editorDesc=Ajoutez et organisez les signets ci-dessous. Cliquez sur + pour ajouter des signets enfants.
|
editTableOfContents.editorDesc=Ajoutez et organisez les signets ci-dessous. Cliquez sur + pour ajouter des signets enfants.
|
||||||
editTableOfContents.addBookmark=Ajouter un nouveau signet
|
editTableOfContents.addBookmark=Ajouter un nouveau signet
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=Cet outil vous permet d'ajouter ou de modifier la table des matières (signets) dans un document PDF.
|
editTableOfContents.desc.1=Cet outil vous permet d'ajouter ou de modifier la table des matières (signets) dans un document PDF.
|
||||||
editTableOfContents.desc.2=Vous pouvez créer une structure hiérarchique en ajoutant des signets enfants à des signets parents.
|
editTableOfContents.desc.2=Vous pouvez créer une structure hiérarchique en ajoutant des signets enfants à des signets parents.
|
||||||
editTableOfContents.desc.3=Chaque signet nécessite un titre et un numéro de page cible.
|
editTableOfContents.desc.3=Chaque signet nécessite un titre et un numéro de page cible.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Coitianta
|
|||||||
settings.title=Socruithe
|
settings.title=Socruithe
|
||||||
settings.update=Nuashonrú ar fáil
|
settings.update=Nuashonrú ar fáil
|
||||||
settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil.
|
settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Leagan Aipe:
|
settings.appVersion=Leagan Aipe:
|
||||||
settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip):
|
settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip):
|
||||||
settings.downloadOption.1=Oscail sa bhfuinneog chéanna
|
settings.downloadOption.1=Oscail sa bhfuinneog chéanna
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Cineál dath
|
|||||||
pdfToImage.color=Dath
|
pdfToImage.color=Dath
|
||||||
pdfToImage.grey=Scála Liath
|
pdfToImage.grey=Scála Liath
|
||||||
pdfToImage.blackwhite=Dubh agus Bán (D’fhéadfadh sonraí a chailleadh!)
|
pdfToImage.blackwhite=Dubh agus Bán (D’fhéadfadh sonraí a chailleadh!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Tiontaigh
|
pdfToImage.submit=Tiontaigh
|
||||||
pdfToImage.info=Níl Python suiteáilte. Ag teastáil le haghaidh comhshó WebP.
|
pdfToImage.info=Níl Python suiteáilte. Ag teastáil le haghaidh comhshó WebP.
|
||||||
pdfToImage.placeholder=(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1)
|
pdfToImage.placeholder=(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=लोकप्रिय
|
|||||||
settings.title=सेटिंग्स
|
settings.title=सेटिंग्स
|
||||||
settings.update=अपडेट उपलब्ध है
|
settings.update=अपडेट उपलब्ध है
|
||||||
settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है।
|
settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है।
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=ऐप संस्करण:
|
settings.appVersion=ऐप संस्करण:
|
||||||
settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए):
|
settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए):
|
||||||
settings.downloadOption.1=उसी विंडो में खोलें
|
settings.downloadOption.1=उसी विंडो में खोलें
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=रंग प्रकार
|
|||||||
pdfToImage.color=रंग
|
pdfToImage.color=रंग
|
||||||
pdfToImage.grey=ग्रेस्केल
|
pdfToImage.grey=ग्रेस्केल
|
||||||
pdfToImage.blackwhite=काला और सफेद (डेटा खो सकता है!)
|
pdfToImage.blackwhite=काला और सफेद (डेटा खो सकता है!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=बदलें
|
pdfToImage.submit=बदलें
|
||||||
pdfToImage.info=Python स्थापित नहीं है। WebP रूपांतरण के लिए आवश्यक है।
|
pdfToImage.info=Python स्थापित नहीं है। WebP रूपांतरण के लिए आवश्यक है।
|
||||||
pdfToImage.placeholder=(जैसे 1,2,8 या 4,7,12-16 या 2n-1)
|
pdfToImage.placeholder=(जैसे 1,2,8 या 4,7,12-16 या 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popularno
|
|||||||
settings.title=Postavke
|
settings.title=Postavke
|
||||||
settings.update=Dostupno ažuriranje
|
settings.update=Dostupno ažuriranje
|
||||||
settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}).
|
settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}).
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Verzija aplikacije:
|
settings.appVersion=Verzija aplikacije:
|
||||||
settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata):
|
settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata):
|
||||||
settings.downloadOption.1=Otvori u istom prozoru
|
settings.downloadOption.1=Otvori u istom prozoru
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Tip boje
|
|||||||
pdfToImage.color=Boja
|
pdfToImage.color=Boja
|
||||||
pdfToImage.grey=Sivi tonovi
|
pdfToImage.grey=Sivi tonovi
|
||||||
pdfToImage.blackwhite=Crno-bijelo (mogu se izgubiti podaci!)
|
pdfToImage.blackwhite=Crno-bijelo (mogu se izgubiti podaci!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Pretvori
|
pdfToImage.submit=Pretvori
|
||||||
pdfToImage.info=Python nije instaliran. Treba je za konverziju na WebP.
|
pdfToImage.info=Python nije instaliran. Treba je za konverziju na WebP.
|
||||||
pdfToImage.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1)
|
pdfToImage.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Népszerű
|
|||||||
settings.title=Beállítások
|
settings.title=Beállítások
|
||||||
settings.update=Frissítés elérhető
|
settings.update=Frissítés elérhető
|
||||||
settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el.
|
settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Sürgős frissítés érhető el
|
||||||
|
update.updateAvailable=Frissítés érhető el
|
||||||
|
update.modalTitle=Frissítés érhető el
|
||||||
|
update.current=Jelenlegi verzió
|
||||||
|
update.latest=Legújabb verzió
|
||||||
|
update.latestStable=Legújabb stabil verzió
|
||||||
|
update.priority=Fontosság
|
||||||
|
update.recommendedAction=Ajánlott lépés
|
||||||
|
update.breakingChangesDetected=⚠️ Jelentős változások észlelve
|
||||||
|
update.breakingChangesMessage=Ez a frissítés jelentős változásokat tartalmaz. Kérjük, olvassa el az alábbi migrációs útmutatót.
|
||||||
|
update.migrationGuides=Migrációs útmutatók:
|
||||||
|
update.viewGuide=Útmutató megtekintése
|
||||||
|
update.loadingDetailedInfo=Részletes verzióinformációk betöltése folyamatban...
|
||||||
|
update.close=Bezárás
|
||||||
|
update.viewAllReleases=Összes kiadás megtekintése
|
||||||
|
update.downloadLatest=Legújabb verzió letöltése
|
||||||
|
update.availableUpdates=Elérhető frissítések:
|
||||||
|
update.unableToLoadDetails=Nem sikerült betölteni a részletes verzióinformációkat.
|
||||||
|
update.version=Verzió
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=SÜRGETŐ
|
||||||
|
update.priority.normal=NORMÁL
|
||||||
|
update.priority.minor=KISEBB
|
||||||
|
update.priority.low=ALACSONY
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Megszakító változások:
|
||||||
|
update.breakingChangesDefault=Ez a verzió megszakító változásokat tartalmaz
|
||||||
|
update.migrationGuide=Migrációs útmutató
|
||||||
settings.appVersion=Alkalmazás verziója:
|
settings.appVersion=Alkalmazás verziója:
|
||||||
settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén):
|
settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén):
|
||||||
settings.downloadOption.1=Megnyitás ugyanabban az ablakban
|
settings.downloadOption.1=Megnyitás ugyanabban az ablakban
|
||||||
@ -1282,7 +1314,7 @@ merge.header=Több PDF egyesítése (2+)
|
|||||||
merge.sortByName=Rendezés név szerint
|
merge.sortByName=Rendezés név szerint
|
||||||
merge.sortByDate=Rendezés dátum szerint
|
merge.sortByDate=Rendezés dátum szerint
|
||||||
merge.removeCertSign=Digitális aláírás eltávolítása az egyesített fájlban?
|
merge.removeCertSign=Digitális aláírás eltávolítása az egyesített fájlban?
|
||||||
merge.generateToc=Generate table of contents in the merged file?
|
merge.generateToc=Tartalomjegyzék létrehozása az egyesített fájlban?
|
||||||
merge.submit=Egyesítés
|
merge.submit=Egyesítés
|
||||||
|
|
||||||
|
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Színtípus
|
|||||||
pdfToImage.color=Színes
|
pdfToImage.color=Színes
|
||||||
pdfToImage.grey=Szürkeárnyalatos
|
pdfToImage.grey=Szürkeárnyalatos
|
||||||
pdfToImage.blackwhite=Fekete-fehér (adatvesztéssel járhat!)
|
pdfToImage.blackwhite=Fekete-fehér (adatvesztéssel járhat!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Konvertálás
|
pdfToImage.submit=Konvertálás
|
||||||
pdfToImage.info=Python nincs telepítve. WebP konverzióhoz szükséges.
|
pdfToImage.info=Python nincs telepítve. WebP konverzióhoz szükséges.
|
||||||
pdfToImage.placeholder=(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1)
|
pdfToImage.placeholder=(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Meglévő könyvjelzők cseréje (törölje
|
|||||||
editTableOfContents.editorTitle=Könyvjelző szerkesztő
|
editTableOfContents.editorTitle=Könyvjelző szerkesztő
|
||||||
editTableOfContents.editorDesc=Könyvjelzők hozzáadása és rendezése lent. Kattintson a + gombra gyermek könyvjelzők hozzáadásához.
|
editTableOfContents.editorDesc=Könyvjelzők hozzáadása és rendezése lent. Kattintson a + gombra gyermek könyvjelzők hozzáadásához.
|
||||||
editTableOfContents.addBookmark=Új könyvjelző hozzáadása
|
editTableOfContents.addBookmark=Új könyvjelző hozzáadása
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=Ez az eszköz lehetővé teszi a tartalomjegyzék (könyvjelzők) hozzáadását vagy szerkesztését egy PDF dokumentumban.
|
editTableOfContents.desc.1=Ez az eszköz lehetővé teszi a tartalomjegyzék (könyvjelzők) hozzáadását vagy szerkesztését egy PDF dokumentumban.
|
||||||
editTableOfContents.desc.2=Hierarchikus struktúrákat hozhat létre, ha gyermek könyvjelzőket ad a szülő könyvjelzőkhöz.
|
editTableOfContents.desc.2=Hierarchikus struktúrákat hozhat létre, ha gyermek könyvjelzőket ad a szülő könyvjelzőkhöz.
|
||||||
editTableOfContents.desc.3=Minden könyvjelzőhöz szükséges egy cím és egy céloldalszám.
|
editTableOfContents.desc.3=Minden könyvjelzőhöz szükséges egy cím és egy céloldalszám.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Populer
|
|||||||
settings.title=Pengaturan
|
settings.title=Pengaturan
|
||||||
settings.update=Pembaruan tersedia
|
settings.update=Pembaruan tersedia
|
||||||
settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia.
|
settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Versi Aplikasi:
|
settings.appVersion=Versi Aplikasi:
|
||||||
settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip):
|
settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip):
|
||||||
settings.downloadOption.1=Buka di jendela yang sama
|
settings.downloadOption.1=Buka di jendela yang sama
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Tipe warna
|
|||||||
pdfToImage.color=Warna
|
pdfToImage.color=Warna
|
||||||
pdfToImage.grey=Skala abu-abu
|
pdfToImage.grey=Skala abu-abu
|
||||||
pdfToImage.blackwhite=Black and White (Bisa kehilangan data!)
|
pdfToImage.blackwhite=Black and White (Bisa kehilangan data!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Konversi
|
pdfToImage.submit=Konversi
|
||||||
pdfToImage.info=Python tidak terinstal. Diperlukan untuk konversi WebP.
|
pdfToImage.info=Python tidak terinstal. Diperlukan untuk konversi WebP.
|
||||||
pdfToImage.placeholder=(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1)
|
pdfToImage.placeholder=(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=Popolare
|
|||||||
settings.title=Impostazioni
|
settings.title=Impostazioni
|
||||||
settings.update=Aggiornamento disponibile
|
settings.update=Aggiornamento disponibile
|
||||||
settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile.
|
settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Aggiornamento disponibile
|
||||||
|
update.updateAvailable=Aggiornamento disponibile
|
||||||
|
update.modalTitle=Aggiornamento disponibile
|
||||||
|
update.current=Corrente
|
||||||
|
update.latest=Ultimo
|
||||||
|
update.latestStable=Ultima versione stabile
|
||||||
|
update.priority=Priorità
|
||||||
|
update.recommendedAction=Azione consigliata
|
||||||
|
update.breakingChangesDetected=⚠️ Rilevate modifiche sostanziali
|
||||||
|
update.breakingChangesMessage=Questo aggiornamento contiene modifiche sostanziali. Consulta le guide alla migrazione riportate di seguito.
|
||||||
|
update.migrationGuides=Guide alla migrazione:
|
||||||
|
update.viewGuide=Visualizza la guida
|
||||||
|
update.loadingDetailedInfo=Caricamento delle informazioni dettagliate sulla versione...
|
||||||
|
update.close=Chiudi
|
||||||
|
update.viewAllReleases=Visualizza tutte le versioni
|
||||||
|
update.downloadLatest=Scarica l'ultima
|
||||||
|
update.availableUpdates=Aggiornamenti disponibili:
|
||||||
|
update.unableToLoadDetails=Impossibile caricare informazioni dettagliate sulla versione.
|
||||||
|
update.version=Versione
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENTE
|
||||||
|
update.priority.normal=NORMALE
|
||||||
|
update.priority.minor=MINORE
|
||||||
|
update.priority.low=BASSA
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Modifiche sostanziali:
|
||||||
|
update.breakingChangesDefault=Questa versione contiene modifiche sostanziali
|
||||||
|
update.migrationGuide=Guida alla migrazione
|
||||||
settings.appVersion=Versione App:
|
settings.appVersion=Versione App:
|
||||||
settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
|
settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
|
||||||
settings.downloadOption.1=Apri in questa finestra
|
settings.downloadOption.1=Apri in questa finestra
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=Tipo di colore
|
|||||||
pdfToImage.color=A colori
|
pdfToImage.color=A colori
|
||||||
pdfToImage.grey=Scala di grigi
|
pdfToImage.grey=Scala di grigi
|
||||||
pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!)
|
pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=Converti
|
pdfToImage.submit=Converti
|
||||||
pdfToImage.info=Python non è installato.È richiesto per la conversione WebP.
|
pdfToImage.info=Python non è installato.È richiesto per la conversione WebP.
|
||||||
pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1)
|
pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1)
|
||||||
@ -1663,7 +1696,7 @@ fileChooser.dragAndDrop=Trascina & Rilascia
|
|||||||
fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF
|
fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF
|
||||||
fileChooser.dragAndDropImage=Trascina & rilascia il file immagine
|
fileChooser.dragAndDropImage=Trascina & rilascia il file immagine
|
||||||
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
|
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
|
||||||
fileChooser.extractPDF=Estraendo...
|
fileChooser.extractPDF=Estrazione...
|
||||||
fileChooser.addAttachments=trascina & rilascia gli allegati qui
|
fileChooser.addAttachments=trascina & rilascia gli allegati qui
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deselezi
|
|||||||
editTableOfContents.editorTitle=Editor segnalibri
|
editTableOfContents.editorTitle=Editor segnalibri
|
||||||
editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari.
|
editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari.
|
||||||
editTableOfContents.addBookmark=Aggiungi nuovo segnalibro
|
editTableOfContents.addBookmark=Aggiungi nuovo segnalibro
|
||||||
|
editTableOfContents.importBookmarksDefault=Importa
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Carica file JSON
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Incolla dagli appunti
|
||||||
|
editTableOfContents.exportBookmarksDefault=Esporta
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Scarica come JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copia come testo
|
||||||
editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF.
|
editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF.
|
||||||
editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali.
|
editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali.
|
||||||
editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione.
|
editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione.
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=人気
|
|||||||
settings.title=設定
|
settings.title=設定
|
||||||
settings.update=利用可能なアップデート
|
settings.update=利用可能なアップデート
|
||||||
settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。
|
settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=Appバージョン:
|
settings.appVersion=Appバージョン:
|
||||||
settings.downloadOption.title=ダウンロードオプション(zip以外の単一ファイル):
|
settings.downloadOption.title=ダウンロードオプション(zip以外の単一ファイル):
|
||||||
settings.downloadOption.1=同じウィンドウで開く
|
settings.downloadOption.1=同じウィンドウで開く
|
||||||
@ -570,11 +602,11 @@ rotate.tags=server side
|
|||||||
|
|
||||||
home.imageToPdf.title=画像をPDFに変換
|
home.imageToPdf.title=画像をPDFに変換
|
||||||
home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。
|
home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。
|
||||||
imageToPdf.tags=conversion,img,jpg,picture,photo
|
imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfToImage.title=PDFを画像に変換
|
home.pdfToImage.title=PDFを画像に変換
|
||||||
home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。
|
home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。
|
||||||
pdfToImage.tags=conversion,img,jpg,picture,photo
|
pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop
|
||||||
|
|
||||||
home.pdfOrganiser.title=整理
|
home.pdfOrganiser.title=整理
|
||||||
home.pdfOrganiser.desc=ページの削除/並べ替えします。
|
home.pdfOrganiser.desc=ページの削除/並べ替えします。
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=カラーモード
|
|||||||
pdfToImage.color=カラー
|
pdfToImage.color=カラー
|
||||||
pdfToImage.grey=グレースケール
|
pdfToImage.grey=グレースケール
|
||||||
pdfToImage.blackwhite=白黒(データが失われる可能性があります!)
|
pdfToImage.blackwhite=白黒(データが失われる可能性があります!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=変換
|
pdfToImage.submit=変換
|
||||||
pdfToImage.info=Pythonがインストールされていません。WebPの変換に必要です。
|
pdfToImage.info=Pythonがインストールされていません。WebPの変換に必要です。
|
||||||
pdfToImage.placeholder=(例:1,2,8、4,7,12-16、2n-1)
|
pdfToImage.placeholder=(例:1,2,8、4,7,12-16、2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=既存のしおりを置き換える(既
|
|||||||
editTableOfContents.editorTitle=しおりエディター
|
editTableOfContents.editorTitle=しおりエディター
|
||||||
editTableOfContents.editorDesc=以下にしおりを追加して配置します。+をクリックして、子のしおりを追加します。
|
editTableOfContents.editorDesc=以下にしおりを追加して配置します。+をクリックして、子のしおりを追加します。
|
||||||
editTableOfContents.addBookmark=新しいしおりを追加
|
editTableOfContents.addBookmark=新しいしおりを追加
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=このツールを使用すると、PDFドキュメントに目次(しおり)を追加または編集できます。
|
editTableOfContents.desc.1=このツールを使用すると、PDFドキュメントに目次(しおり)を追加または編集できます。
|
||||||
editTableOfContents.desc.2=親しおりに子しおりを追加することで階層構造を作成できます。
|
editTableOfContents.desc.2=親しおりに子しおりを追加することで階層構造を作成できます。
|
||||||
editTableOfContents.desc.3=各しおりにはタイトルと対象のページ番号が必要です。
|
editTableOfContents.desc.3=各しおりにはタイトルと対象のページ番号が必要です。
|
||||||
|
@ -366,6 +366,38 @@ navbar.sections.popular=인기
|
|||||||
settings.title=설정
|
settings.title=설정
|
||||||
settings.update=업데이트 가능
|
settings.update=업데이트 가능
|
||||||
settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다.
|
settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다.
|
||||||
|
|
||||||
|
# Update modal and notification strings
|
||||||
|
update.urgentUpdateAvailable=🚨 Update Available
|
||||||
|
update.updateAvailable=Update Available
|
||||||
|
update.modalTitle=Update Available
|
||||||
|
update.current=Current
|
||||||
|
update.latest=Latest
|
||||||
|
update.latestStable=Latest Stable
|
||||||
|
update.priority=Priority
|
||||||
|
update.recommendedAction=Recommended Action
|
||||||
|
update.breakingChangesDetected=⚠️ Breaking Changes Detected
|
||||||
|
update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below.
|
||||||
|
update.migrationGuides=Migration Guides:
|
||||||
|
update.viewGuide=View Guide
|
||||||
|
update.loadingDetailedInfo=Loading detailed version information...
|
||||||
|
update.close=Close
|
||||||
|
update.viewAllReleases=View All Releases
|
||||||
|
update.downloadLatest=Download Latest
|
||||||
|
update.availableUpdates=Available Updates:
|
||||||
|
update.unableToLoadDetails=Unable to load detailed version information.
|
||||||
|
update.version=Version
|
||||||
|
|
||||||
|
# Update priority levels
|
||||||
|
update.priority.urgent=URGENT
|
||||||
|
update.priority.normal=NORMAL
|
||||||
|
update.priority.minor=MINOR
|
||||||
|
update.priority.low=LOW
|
||||||
|
|
||||||
|
# Breaking changes text
|
||||||
|
update.breakingChanges=Breaking Changes:
|
||||||
|
update.breakingChangesDefault=This version contains breaking changes
|
||||||
|
update.migrationGuide=Migration Guide
|
||||||
settings.appVersion=앱 버전:
|
settings.appVersion=앱 버전:
|
||||||
settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용):
|
settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용):
|
||||||
settings.downloadOption.1=같은 창에서 열기
|
settings.downloadOption.1=같은 창에서 열기
|
||||||
@ -1402,6 +1434,7 @@ pdfToImage.colorType=색상 유형
|
|||||||
pdfToImage.color=컬러
|
pdfToImage.color=컬러
|
||||||
pdfToImage.grey=그레이스케일
|
pdfToImage.grey=그레이스케일
|
||||||
pdfToImage.blackwhite=흑백 (데이터 손실 가능성 있음!)
|
pdfToImage.blackwhite=흑백 (데이터 손실 가능성 있음!)
|
||||||
|
pdfToImage.dpi=DPI (The server limit is {0} dpi)
|
||||||
pdfToImage.submit=변환
|
pdfToImage.submit=변환
|
||||||
pdfToImage.info=WebP 변환에는 Python이 필요합니다. Python이 설치되지 않았습니다.
|
pdfToImage.info=WebP 변환에는 Python이 필요합니다. Python이 설치되지 않았습니다.
|
||||||
pdfToImage.placeholder=(예: 1,2,8 또는 4,7,12-16 또는 2n-1)
|
pdfToImage.placeholder=(예: 1,2,8 또는 4,7,12-16 또는 2n-1)
|
||||||
@ -1859,6 +1892,12 @@ editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to appen
|
|||||||
editTableOfContents.editorTitle=Bookmark Editor
|
editTableOfContents.editorTitle=Bookmark Editor
|
||||||
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
|
||||||
editTableOfContents.addBookmark=Add New Bookmark
|
editTableOfContents.addBookmark=Add New Bookmark
|
||||||
|
editTableOfContents.importBookmarksDefault=Import
|
||||||
|
editTableOfContents.importBookmarksFromJsonFile=Upload JSON file
|
||||||
|
editTableOfContents.importBookmarksFromClipboard=Paste from clipboard
|
||||||
|
editTableOfContents.exportBookmarksDefault=Export
|
||||||
|
editTableOfContents.exportBookmarksAsJson=Download as JSON
|
||||||
|
editTableOfContents.exportBookmarksAsText=Copy as text
|
||||||
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
|
||||||
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
|
||||||
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user