mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364eb9cabe |
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(chmod:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.format.enabled": true,
|
||||
"java.format.settings.profile": "GoogleStyle",
|
||||
"java.format.settings.google.version": "1.28.0",
|
||||
"java.format.settings.google.version": "1.26.0",
|
||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||
"java.saveActions.cleanup": true,
|
||||
"java.cleanup.actions": [
|
||||
@@ -79,17 +79,9 @@
|
||||
".venv*/",
|
||||
".vscode/",
|
||||
"bin/",
|
||||
"app/core/bin/",
|
||||
"app/common/bin/",
|
||||
"app/proprietary/bin/",
|
||||
"build/",
|
||||
"app/core/build/",
|
||||
"app/common/build/",
|
||||
"app/proprietary/build/",
|
||||
"configs/",
|
||||
"app/core/configs/",
|
||||
"customFiles/",
|
||||
"app/core/customFiles/",
|
||||
"docs/",
|
||||
"exampleYmlFiles",
|
||||
"gradle/",
|
||||
@@ -101,9 +93,6 @@
|
||||
".git-blame-ignore-revs",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
"app/core/.gitignore",
|
||||
"app/common/.gitignore",
|
||||
"app/proprietary/.gitignore",
|
||||
".pre-commit-config.yaml"
|
||||
],
|
||||
"java.signatureHelp.enabled": true,
|
||||
@@ -130,9 +119,7 @@
|
||||
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
|
||||
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
||||
"charliermarsh.ruff", // Ruff extension for Ruff language support
|
||||
"github.vscode-github-actions", // GitHub Actions extension for Visual Studio Code
|
||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||
"redhat.vscode-yaml" // YAML extension for Visual Studio Code
|
||||
"github.vscode-github-actions" // GitHub Actions extension for Visual Studio Code
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
+8
-2
@@ -24,19 +24,25 @@ indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
|
||||
[*.js]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||
indent_size = 2
|
||||
|
||||
[*.{yml,yaml}]
|
||||
[*.yaml]
|
||||
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.yml]
|
||||
# YML files follow the same conventions as YAML files, using an indent size of 2 spaces.
|
||||
indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
# Formatting
|
||||
5f771b785130154ed47952635b7acef371ffe0ec
|
||||
7fa5e130d99227c2202ebddfdd91348176ec0c7b
|
||||
14d4fbb2a36195eedb034785e5a5ff6a47f268c6
|
||||
ee8030c1c4148062cde15c49c67d04ef03930c55
|
||||
fcd41924f5f261febfa9d9a92994671f3ebc97d6
|
||||
|
||||
# Normalize files
|
||||
55d4fda01b2f39f5b7d7b4fda5214bd7ff0fd5dd
|
||||
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# Ignore all JavaScript files in a directory
|
||||
app/core/src/main/resources/static/pdfjs/* linguist-vendored
|
||||
app/core/src/main/resources/static/pdfjs/** linguist-vendored
|
||||
app/core/src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||
app/core/src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||
app/core/src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||
app/core/src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||
app/core/src/main/resources/static/css/fonts/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs/** linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||
stirling-pdf/src/main/resources/static/css/fonts/* linguist-vendored
|
||||
|
||||
+2
-21
@@ -1,21 +1,2 @@
|
||||
# All PRs must be approved by Frooodle or Ludy87
|
||||
* @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
|
||||
# All PRs to V1 must be approved by Frooodle
|
||||
* @Frooodle @reecebrowne @Ludy87 @DarioGii @ConnorYoh
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
build: &build
|
||||
- build.gradle
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
|
||||
app: &app
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
|
||||
openapi: &openapi
|
||||
- build.gradle
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
|
||||
project: &project
|
||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*'
|
||||
- exampleYmlFiles/**
|
||||
- gradle/**
|
||||
- libs/**
|
||||
- testing/**
|
||||
- build.gradle
|
||||
- Dockerfile
|
||||
- Dockerfile.fat
|
||||
- Dockerfile.ultra-lite
|
||||
- gradle.properties
|
||||
- gradlew
|
||||
- gradlew.bat
|
||||
- launch4jConfig.xml
|
||||
- settings.gradle
|
||||
- frontend/**
|
||||
- docker/**
|
||||
@@ -7,7 +7,6 @@
|
||||
"sbplat",
|
||||
"reecebrowne",
|
||||
"DarioGii",
|
||||
"ConnorYoh",
|
||||
"EthanHealy01"
|
||||
"ConnorYoh"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,106 +2,88 @@ version: 1
|
||||
labels:
|
||||
|
||||
- label: "Bugfix"
|
||||
title: '^fix(\([^)]*\))?:|^fix:.*'
|
||||
title: '^fix:.*'
|
||||
|
||||
- label: "enhancement"
|
||||
title: '^feat(\([^)]*\))?:|^feat:.*'
|
||||
title: '^feat:.*'
|
||||
|
||||
- label: "build"
|
||||
title: '^build(\([^)]*\))?:|^build:.*'
|
||||
title: '^build:.*'
|
||||
|
||||
- label: "chore"
|
||||
title: '^chore(\([^)]*\))?:|^chore:.*'
|
||||
title: '^chore:.*'
|
||||
|
||||
- label: "ci"
|
||||
title: '^ci(\([^)]*\))?:|^ci:.*'
|
||||
|
||||
- label: "ci"
|
||||
title: '^.*\(ci\):.*'
|
||||
title: '^ci:.*'
|
||||
|
||||
- label: "perf"
|
||||
title: '^perf(\([^)]*\))?:|^perf:.*'
|
||||
title: '^perf:.*'
|
||||
|
||||
- label: "refactor"
|
||||
title: '^refactor(\([^)]*\))?:|^refactor:.*'
|
||||
title: '^refactor:.*'
|
||||
|
||||
- label: "revert"
|
||||
title: '^revert(\([^)]*\))?:|^revert:.*'
|
||||
title: '^revert:.*'
|
||||
|
||||
- label: "style"
|
||||
title: '^style(\([^)]*\))?:|^style:.*'
|
||||
title: '^style:.*'
|
||||
|
||||
- label: "Documentation"
|
||||
title: '^docs(\([^)]*\))?:|^docs:.*'
|
||||
|
||||
- label: "Documentation"
|
||||
title: '^.*\(docs\):.*'
|
||||
|
||||
- label: "dependencies"
|
||||
title: '^deps(\([^)]*\))?:|^deps:.*'
|
||||
|
||||
- label: "dependencies"
|
||||
title: '^.*\(deps\):.*'
|
||||
title: '^docs:.*'
|
||||
|
||||
- label: 'API'
|
||||
title: '.*openapi.*|.*swagger.*|.*api.*'
|
||||
|
||||
- label: 'v2'
|
||||
base-branch: 'V2'
|
||||
title: '.*openapi.*'
|
||||
|
||||
- label: 'Translation'
|
||||
files:
|
||||
- 'app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
|
||||
- 'stirling-pdf/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}.properties'
|
||||
- 'scripts/ignore_translation.toml'
|
||||
- 'app/core/src/main/resources/templates/fragments/languages.html'
|
||||
- 'stirling-pdf/src/main/resources/templates/fragments/languages.html'
|
||||
- '.github/scripts/check_language_properties.py'
|
||||
|
||||
- label: 'Front End'
|
||||
files:
|
||||
- 'app/core/src/main/resources/templates/.*'
|
||||
- 'app/proprietary/src/main/resources/templates/.*'
|
||||
- 'app/core/src/main/resources/static/.*'
|
||||
- 'app/proprietary/src/main/resources/static/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
- 'frontend/**'
|
||||
- 'stirling-pdf/src/main/resources/templates/.*'
|
||||
- 'proprietary/src/main/resources/templates/.*'
|
||||
- 'stirling-pdf/src/main/resources/static/.*'
|
||||
- 'proprietary/src/main/resources/static/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/.*'
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||
|
||||
- label: 'Java'
|
||||
files:
|
||||
- 'app/common/src/main/java/.*.java'
|
||||
- 'app/proprietary/src/main/java/.*.java'
|
||||
- 'app/core/src/main/java/.*.java'
|
||||
- 'common/src/main/java/.*.java'
|
||||
- 'proprietary/src/main/java/.*.java'
|
||||
- 'stirling-pdf/src/main/java/.*.java'
|
||||
|
||||
- label: 'Back End'
|
||||
files:
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/config/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/.*'
|
||||
- 'app/core/src/main/resources/settings.yml.template'
|
||||
- 'app/core/src/main/resources/application.properties'
|
||||
- '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/split_photos.py'
|
||||
- 'app/core/src/main/resources/static/pipeline/defaultWebUIConfigs/**'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/config/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/.*'
|
||||
- 'stirling-pdf/src/main/resources/settings.yml.template'
|
||||
- 'stirling-pdf/src/main/resources/application.properties'
|
||||
- 'stirling-pdf/src/main/resources/banner.txt'
|
||||
- 'scripts/png_to_webp.py'
|
||||
- 'split_photos.py'
|
||||
- 'application.properties'
|
||||
|
||||
- label: 'Security'
|
||||
files:
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/.*'
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/.*'
|
||||
- 'scripts/download-security-jar.sh'
|
||||
- '.github/workflows/dependency-review.yml'
|
||||
- '.github/workflows/scorecards.yml'
|
||||
|
||||
- label: 'API'
|
||||
files:
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/controller/api/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/model/api/.*'
|
||||
- 'app/core/src/main/java/stirling/software/SPDF/service/ApiDocService.java'
|
||||
- 'app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*'
|
||||
- 'app/core/src/main/resources/static/python/png_to_webp.py'
|
||||
- 'app/core/src/main/resources/static/python/split_photos.py'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/.*'
|
||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/.*'
|
||||
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*'
|
||||
- 'scripts/png_to_webp.py'
|
||||
- 'split_photos.py'
|
||||
- '.github/workflows/swagger.yml'
|
||||
|
||||
- label: 'Documentation'
|
||||
@@ -124,7 +106,6 @@ labels:
|
||||
- 'scripts/installFonts.sh'
|
||||
- 'test.sh'
|
||||
- 'test2.sh'
|
||||
- 'docker/**'
|
||||
|
||||
- label: 'Devtools'
|
||||
files:
|
||||
@@ -135,13 +116,12 @@ labels:
|
||||
- '.pre-commit-config'
|
||||
- '.github/workflows/pre_commit.yml'
|
||||
- 'devGuide/.*'
|
||||
- 'devTools/.*'
|
||||
|
||||
- label: 'Test'
|
||||
files:
|
||||
- 'app/common/src/test/.*'
|
||||
- 'app/proprietary/src/test/.*'
|
||||
- 'app/core/src/test/.*'
|
||||
- 'common/src/test/.*'
|
||||
- 'proprietary/src/test/.*'
|
||||
- 'stirling-pdf/src/test/.*'
|
||||
- 'testing/.*'
|
||||
- '.github/workflows/scorecards.yml'
|
||||
- 'exampleYmlFiles/test_cicd.yml'
|
||||
@@ -157,6 +137,6 @@ labels:
|
||||
- 'gradlew.bat'
|
||||
- 'settings.gradle'
|
||||
- 'build.gradle'
|
||||
- 'app/common/build.gradle'
|
||||
- 'app/proprietary/build.gradle'
|
||||
- 'app/core/build.gradle'
|
||||
- 'common/build.gradle'
|
||||
- 'proprietary/build.gradle'
|
||||
- 'stirling-pdf/build.gradle'
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
Translation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/messages_*_*.properties'
|
||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/templates/fragments/languages.html'
|
||||
|
||||
Front End:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/templates/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/static/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/**'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/**/*'
|
||||
|
||||
Java:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'common/src/main/java/**/*.java'
|
||||
- any-glob-to-any-file: 'proprietary/src/main/java/**/*.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/**/*.java'
|
||||
|
||||
Back End:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/config/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/settings.yml.template'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/application.properties'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/resources/banner.txt'
|
||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
||||
- any-glob-to-any-file: 'split_photos.py'
|
||||
|
||||
Security:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'proprietary/src/main/java/stirling/software/proprietary/security/**/*'
|
||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
|
||||
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||
|
||||
API:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/**/*'
|
||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
||||
- any-glob-to-any-file: 'split_photos.py'
|
||||
- any-glob-to-any-file: '.github/workflows/swagger.yml'
|
||||
|
||||
Documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '**/*.md'
|
||||
- any-glob-to-any-file: 'scripts/counter_translation.py'
|
||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||
|
||||
Docker:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '.github/workflows/build.yml'
|
||||
- any-glob-to-any-file: '.github/workflows/push-docker.yml'
|
||||
- any-glob-to-any-file: 'Dockerfile'
|
||||
- any-glob-to-any-file: 'Dockerfile.fat'
|
||||
- any-glob-to-any-file: 'Dockerfile.ultra-lite'
|
||||
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||
- any-glob-to-any-file: 'scripts/init.sh'
|
||||
- any-glob-to-any-file: 'scripts/init-without-ocr.sh'
|
||||
- any-glob-to-any-file: 'scripts/installFonts.sh'
|
||||
- any-glob-to-any-file: 'test.sh'
|
||||
- any-glob-to-any-file: 'test2.sh'
|
||||
|
||||
Devtools:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '.devcontainer/**/*'
|
||||
- any-glob-to-any-file: 'Dockerfile.dev'
|
||||
|
||||
Test:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'cucumber/**/*'
|
||||
- any-glob-to-any-file: 'common/src/test/**/*'
|
||||
- any-glob-to-any-file: 'proprietary/src/test/**/*'
|
||||
- any-glob-to-any-file: 'stirling-pdf/src/test/**/*'
|
||||
- any-glob-to-any-file: 'src/testing/**/*'
|
||||
- any-glob-to-any-file: '.pre-commit-config'
|
||||
- any-glob-to-any-file: '.github/workflows/pre_commit.yml'
|
||||
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||
|
||||
Github:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: '.github/**/*'
|
||||
@@ -42,7 +42,6 @@
|
||||
- name: "Front End"
|
||||
color: "BBD2F1"
|
||||
description: "Issues or pull requests related to front-end development"
|
||||
from_name: "frontend"
|
||||
- name: "github-actions"
|
||||
description: "Pull requests that update GitHub Actions code"
|
||||
color: "999999"
|
||||
@@ -78,12 +77,10 @@
|
||||
- name: "Translation"
|
||||
color: "9FABF9"
|
||||
from_name: "translation"
|
||||
description: "Issues or pull requests related to translation"
|
||||
- name: "upstream"
|
||||
color: "DEDEDE"
|
||||
- name: "v2"
|
||||
color: "FFFF00"
|
||||
description: "Issues or pull requests related to the v2 branch"
|
||||
- name: "wontfix"
|
||||
description: "This will not be worked on"
|
||||
color: "FFFFFF"
|
||||
@@ -178,9 +175,3 @@
|
||||
description: "This PR changes 1000+ lines ignoring generated files."
|
||||
- name: "to research"
|
||||
color: "FBCA04"
|
||||
- name: "pr-deployed"
|
||||
color: "00FF00"
|
||||
description: "Pull request has been deployed to a test environment"
|
||||
- name: "codex"
|
||||
color: "ededed"
|
||||
description: "chatgpt AI generated code"
|
||||
|
||||
@@ -197,7 +197,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.getcwd(), "app", "core", "src", "main", "resources")
|
||||
os.path.join(os.getcwd(), "stirling-pdf", "src", "main", "resources")
|
||||
)
|
||||
|
||||
for file_path in file_arr:
|
||||
@@ -219,14 +219,13 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
# only local windows command
|
||||
not file_normpath.startswith(
|
||||
os.path.join(
|
||||
"", "app", "core", "src", "main", "resources", "messages_"
|
||||
"", "stirling-pdf", "src", "main", "resources", "messages_"
|
||||
)
|
||||
)
|
||||
and not file_normpath.startswith(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"app",
|
||||
"core",
|
||||
"stirling-pdf",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
@@ -329,7 +328,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/app/core/src/main/resources/messages_en_GB.properties)"
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/stirling-pdf/src/main/resources/messages_en_GB.properties)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
@@ -390,8 +389,7 @@ if __name__ == "__main__":
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"app",
|
||||
"core",
|
||||
"stirling-pdf",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
|
||||
@@ -2,16 +2,8 @@ name: Auto PR V2 Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: "PR number to deploy"
|
||||
required: true
|
||||
allow_fork:
|
||||
description: "Allow deploying fork PR?"
|
||||
required: false
|
||||
default: "false"
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -20,93 +12,99 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
allow_fork: ${{ steps.decide.outputs.allow_fork }}
|
||||
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.resolve.outputs.repository }}
|
||||
pr_ref: ${{ steps.resolve.outputs.ref }}
|
||||
should_deploy: ${{ steps.check-conditions.outputs.should_deploy }}
|
||||
pr_number: ${{ github.event.number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Resolve PR info
|
||||
id: resolve
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
let prNumber = context.eventName === 'workflow_dispatch'
|
||||
? parseInt(process.env.INPUT_PR, 10)
|
||||
: context.payload.number;
|
||||
|
||||
if (!Number.isInteger(prNumber)) { core.setFailed('Invalid PR number'); return; }
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
core.setOutput('repository', pr.head.repo.full_name);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
core.setOutput('is_fork', String(pr.head.repo.fork));
|
||||
core.setOutput('base_ref', pr.base.ref);
|
||||
core.setOutput('author', pr.user.login);
|
||||
core.setOutput('state', pr.state);
|
||||
|
||||
- name: Decide deploy
|
||||
id: decide
|
||||
shell: bash
|
||||
- name: Check deployment conditions
|
||||
id: check-conditions
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
STATE: ${{ steps.resolve.outputs.state }}
|
||||
IS_FORK: ${{ steps.resolve.outputs.is_fork }}
|
||||
# nur bei workflow_dispatch gesetzt:
|
||||
ALLOW_FORK_INPUT: ${{ inputs.allow_fork }}
|
||||
# für Auto-PR-Logik:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
PR_BASE: ${{ steps.resolve.outputs.base_ref }}
|
||||
PR_AUTHOR: ${{ steps.resolve.outputs.author }}
|
||||
run: |
|
||||
set -e
|
||||
# Standard: nichts deployen
|
||||
should=false
|
||||
allow_fork="$(echo "${ALLOW_FORK_INPUT:-false}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
if [ "$STATE" != "open" ]; then
|
||||
echo "PR not open -> skip"
|
||||
else
|
||||
if [ "$IS_FORK" = "true" ] && [ "$allow_fork" != "true" ]; then
|
||||
echo "Fork PR and allow_fork=false -> skip"
|
||||
else
|
||||
should=true
|
||||
fi
|
||||
echo "PR Title: $PR_TITLE"
|
||||
echo "PR Author: $PR_AUTHOR"
|
||||
echo "PR Branch: $PR_BRANCH"
|
||||
echo "PR Base Branch: ${{ github.event.pull_request.base.ref }}"
|
||||
|
||||
# Define authorized users
|
||||
authorized_users=(
|
||||
"Frooodle"
|
||||
"sf298"
|
||||
"Ludy87"
|
||||
"LaserKaspar"
|
||||
"sbplat"
|
||||
"reecebrowne"
|
||||
"DarioGii"
|
||||
"ConnorYoh"
|
||||
)
|
||||
|
||||
# Check if author is in the authorized list
|
||||
is_authorized=false
|
||||
for user in "${authorized_users[@]}"; do
|
||||
if [[ "$PR_AUTHOR" == "$user" ]]; then
|
||||
is_authorized=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# If PR is targeting feature/react-overhaul and user is authorized, deploy unconditionally
|
||||
PR_BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
|
||||
if [[ "$PR_BASE_BRANCH" == "feature/react-overhaul" && "$is_authorized" == "true" ]]; then
|
||||
echo "✅ Deployment forced: PR targets feature/react-overhaul and author is authorized."
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Otherwise, continue with original keyword checks
|
||||
has_v2_keyword=false
|
||||
if [[ "$PR_TITLE" =~ [Vv]2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?2 ]] || [[ "$PR_TITLE" =~ [Vv]ersion.?[Tt]wo ]]; then
|
||||
has_v2_keyword=true
|
||||
fi
|
||||
|
||||
has_branch_keyword=false
|
||||
if [[ "$PR_BRANCH" =~ [Vv]2 ]] || [[ "$PR_BRANCH" =~ [Rr]eact ]]; then
|
||||
has_branch_keyword=true
|
||||
fi
|
||||
|
||||
if [[ "$is_authorized" == "true" && ( "$has_v2_keyword" == "true" || "$has_branch_keyword" == "true" ) ]]; then
|
||||
echo "✅ Deployment conditions met"
|
||||
echo "should_deploy=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96")
|
||||
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
|
||||
if [ "$PR_BASE" = "V2" ] && [ "$is_auth" = true ]; then
|
||||
should=true
|
||||
else
|
||||
title_has_v2=false; echo "$PR_TITLE" | grep -qiE 'v2|version.?2|version.?two' && title_has_v2=true
|
||||
branch_has_kw=false; echo "$PR_BRANCH" | grep -qiE 'v2|react' && branch_has_kw=true
|
||||
if [ "$is_auth" = true ] && { [ "$title_has_v2" = true ] || [ "$branch_has_kw" = true ]; }; then
|
||||
should=true
|
||||
fi
|
||||
fi
|
||||
echo "❌ Deployment conditions not met"
|
||||
echo " - Authorized user: $is_authorized"
|
||||
echo " - Has V2 keyword in title: $has_v2_keyword"
|
||||
echo " - Has V2/React keyword in branch: $has_branch_keyword"
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
echo "should_deploy=$should" >> $GITHUB_OUTPUT
|
||||
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
if: steps.check-conditions.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
# For forks, use the full repository name, for internal PRs use the current repo
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
repository="${{ github.event.pull_request.head.repo.full_name }}"
|
||||
else
|
||||
repository="${{ github.repository }}"
|
||||
fi
|
||||
|
||||
echo "repository=$repository" >> $GITHUB_OUTPUT
|
||||
echo "ref=${{ github.event.pull_request.head.ref }}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||
if: needs.check-pr.outputs.should_deploy == 'true'
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
@@ -118,7 +116,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
@@ -153,13 +151,13 @@ jobs:
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
|
||||
const v2Comments = comments.filter(comment =>
|
||||
comment.body.includes('🚀 **Auto-deploying V2 version**') ||
|
||||
comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') ||
|
||||
comment.body.includes('❌ **V2 Auto-deployment failed**')
|
||||
);
|
||||
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
console.log(`Deleting old V2 comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
@@ -176,6 +174,7 @@ jobs:
|
||||
issue_number: prNumber,
|
||||
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||
});
|
||||
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
@@ -184,7 +183,19 @@ jobs:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build backend
|
||||
run: |
|
||||
export DISABLE_ADDITIONAL_FEATURES=true
|
||||
./gradlew clean build
|
||||
env:
|
||||
STIRLING_PDF_DESKTOP_UI: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
@@ -201,81 +212,13 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
- name: Build and push V2 monolith image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
file: ./docker/monolith/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
@@ -290,17 +233,16 @@ jobs:
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||||
|
||||
# Create docker-compose for V2 with separate frontend and backend
|
||||
# Create docker-compose for V2 monolith
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-v2-backend:
|
||||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
stirling-pdf-v2:
|
||||
container_name: stirling-pdf-v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080" # Backend API port
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
volumes:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
@@ -315,19 +257,6 @@ jobs:
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
|
||||
depends_on:
|
||||
- stirling-pdf-v2-backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
@@ -350,10 +279,7 @@ jobs:
|
||||
docker-compose up -d
|
||||
|
||||
# Clean up unused Docker resources to save space
|
||||
docker system prune -af --volumes || true
|
||||
|
||||
# Clean up old backend/frontend images (older than 2 weeks)
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
docker system prune -af --volumes
|
||||
ENDSSH
|
||||
|
||||
# Set port for output
|
||||
@@ -385,12 +311,10 @@ jobs:
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
`🔗 **V2 Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
||||
|
||||
@@ -401,101 +325,3 @@ jobs:
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Clean up V2 deployment comments
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
|
||||
// Find and delete V2 deployment comments
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(c =>
|
||||
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
|
||||
c.user?.type === "Bot"
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
console.log(`Deleted V2 deployment comment (ID: ${comment.id})`);
|
||||
}
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup V2 deployment
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove V2 containers
|
||||
cd /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove V2 PR-specific directories
|
||||
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up V2 containers by name (in case compose cleanup missed them)
|
||||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "V2 cleanup completed"
|
||||
else
|
||||
echo "V2 PR directory not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
|
||||
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||||
# Only remove PR-specific containers and directories
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
continue-on-error: true
|
||||
@@ -6,18 +6,20 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: write # Required for adding reactions to comments
|
||||
pull-requests: read # Required for reading PR information
|
||||
|
||||
jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
@@ -29,7 +31,6 @@ jobs:
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
@@ -42,18 +43,14 @@ jobs:
|
||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
# Generate GitHub App token
|
||||
- name: Generate GitHub App Token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
@@ -126,7 +123,7 @@ jobs:
|
||||
id: add-eyes-reaction
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`);
|
||||
try {
|
||||
@@ -148,33 +145,28 @@ jobs:
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
- name: Generate GitHub App Token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -196,8 +188,14 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
@@ -299,7 +297,7 @@ jobs:
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||
try {
|
||||
@@ -315,26 +313,11 @@ jobs:
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// add label to PR
|
||||
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: prNumber,
|
||||
labels: ['pr-deployed']
|
||||
});
|
||||
console.log(`Added 'pr-deployed' label to PR #${prNumber}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to add label to PR: ${error.message}`);
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
- name: Add failure reaction to comment
|
||||
if: failure()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
|
||||
try {
|
||||
@@ -354,7 +337,7 @@ jobs:
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
@@ -374,11 +357,3 @@ jobs:
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -f ../private.key docker-compose.yml
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: PR Deployment cleanup
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions:
|
||||
@@ -13,99 +13,25 @@ env:
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
if: github.event.action == 'closed'
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Remove 'pr-deployed' label if present
|
||||
id: remove-label-comment
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
||||
// Get all labels on the PR
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const hasLabel = labels.some(label => label.name === 'pr-deployed');
|
||||
|
||||
if (hasLabel) {
|
||||
console.log("Label 'pr-deployed' found. Removing...");
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
name: 'pr-deployed'
|
||||
});
|
||||
} else {
|
||||
console.log("Label 'pr-deployed' not found. Nothing to do.");
|
||||
}
|
||||
|
||||
// Find existing bot comments about the deployment
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
const deploymentComments = comments.filter(c =>
|
||||
c.body?.includes("## 🚀 PR Test Deployment") &&
|
||||
c.user?.type === "Bot"
|
||||
);
|
||||
|
||||
if (deploymentComments.length > 0) {
|
||||
for (const comment of deploymentComments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
console.log(`Deleted deployment comment (ID: ${comment.id})`);
|
||||
}
|
||||
} else {
|
||||
console.log("No matching deployment comments found.");
|
||||
}
|
||||
|
||||
// Set flag if either label or comment was present
|
||||
const hasDeploymentComment = deploymentComments.length > 0;
|
||||
core.setOutput('present', (hasLabel || hasDeploymentComment) ? 'true' : 'false');
|
||||
|
||||
|
||||
- name: Set up SSH
|
||||
if: steps.remove-label-comment.outputs.present == 'true'
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup PR deployment
|
||||
if: steps.remove-label-comment.outputs.present == 'true'
|
||||
id: cleanup
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
@@ -131,11 +57,3 @@ jobs:
|
||||
echo "NO_CLEANUP_NEEDED"
|
||||
fi
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -f ../private.key
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
- name: AI PR Title Analysis
|
||||
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||
id: ai-title-analysis
|
||||
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1.2.8
|
||||
uses: actions/ai-inference@d645f067d89ee1d5d736a5990e327e504d1c5a4a # v1.1.0
|
||||
with:
|
||||
model: openai/gpt-4o
|
||||
system-prompt-file: ".github/config/system-prompt.txt"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Apply Labels
|
||||
uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
configuration-path: .github/labeler-config.yml
|
||||
sync-labels: true
|
||||
@@ -13,11 +13,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
+54
-193
@@ -1,194 +1,122 @@
|
||||
name: Build and Test Workflow
|
||||
name: Build repo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main", "V2", "V2-gha"]
|
||||
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
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
outputs:
|
||||
build: ${{ steps.changes.outputs.build }}
|
||||
app: ${{ steps.changes.outputs.app }}
|
||||
project: ${{ steps.changes.outputs.project }}
|
||||
openapi: ${{ steps.changes.outputs.openapi }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.3.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@v3.0.2
|
||||
id: changes
|
||||
with:
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||
uses: actions/setup-java@v4.7.1
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
id: check-reports
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
"app/core/build/reports/tests/"
|
||||
"app/core/build/test-results/"
|
||||
"app/common/build/reports/tests/"
|
||||
"app/common/build/test-results/"
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
"stirling-pdf/build/reports/tests/"
|
||||
"stirling-pdf/build/test-results/"
|
||||
"common/build/reports/tests/"
|
||||
"common/build/test-results/"
|
||||
"proprietary/build/reports/tests/"
|
||||
"proprietary/build/test-results/"
|
||||
)
|
||||
missing_reports=()
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
missing_reports+=("$dir")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing_reports[@]} -gt 0 ]; then
|
||||
echo "ERROR: The following required test report directories are missing:"
|
||||
printf '%s\n' "${missing_reports[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All required test report directories are present"
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
stirling-pdf/build/reports/tests/
|
||||
stirling-pdf/build/test-results/
|
||||
stirling-pdf/build/reports/problems/
|
||||
common/build/reports/tests/
|
||||
common/build/test-results/
|
||||
common/build/reports/problems/
|
||||
proprietary/build/reports/tests/
|
||||
proprietary/build/test-results/
|
||||
proprietary/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
- uses: gradle/actions/setup-gradle@v4.4.2
|
||||
- name: Generate OpenAPI documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
|
||||
frontend-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4.1.0
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Lint frontend
|
||||
run: cd frontend && npm run lint
|
||||
- name: Build frontend
|
||||
run: cd frontend && npm run build
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && npm run test -- --run
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4.7.1
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
distribution: "adopt"
|
||||
|
||||
- name: check the licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
path: |
|
||||
build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
|
||||
docker-compose-tests:
|
||||
if: needs.files-changed.outputs.project == 'true'
|
||||
needs: files-changed
|
||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||
# (github.event_name == 'pull_request' &&
|
||||
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
|
||||
@@ -207,18 +135,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
@@ -233,7 +161,6 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: 'pip' # caching pip dependencies
|
||||
cache-dependency-path: ./testing/cucumber/requirements.txt
|
||||
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
@@ -245,69 +172,3 @@ jobs:
|
||||
chmod +x ./testing/test.sh
|
||||
chmod +x ./testing/test_disabledEndpoints.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@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- 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@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
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
|
||||
|
||||
@@ -4,19 +4,7 @@ on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "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
|
||||
- "stirling-pdf/src/main/resources/messages_*.properties"
|
||||
|
||||
permissions:
|
||||
contents: read # Allow read access to repository content
|
||||
@@ -30,12 +18,12 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -79,7 +67,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for properties files, handle case where no matches are found
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt || echo "No matching properties files found in PR"
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^stirling-pdf/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt || echo "No matching properties files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No properties files changed in this PR"
|
||||
@@ -127,9 +115,9 @@ jobs:
|
||||
|
||||
// Filter for relevant files based on the PR changes
|
||||
const changedFiles = files
|
||||
.filter(file =>
|
||||
.filter(file =>
|
||||
file.status !== "removed" &&
|
||||
/^app\/core\/src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file.filename)
|
||||
/^stirling-pdf\/src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file.filename)
|
||||
)
|
||||
.map(file => file.filename);
|
||||
|
||||
@@ -169,12 +157,12 @@ jobs:
|
||||
|
||||
// Determine reference file
|
||||
let referenceFilePath;
|
||||
if (changedFiles.includes("app/core/src/main/resources/messages_en_GB.properties")) {
|
||||
if (changedFiles.includes("stirling-pdf/src/main/resources/messages_en_GB.properties")) {
|
||||
console.log("Using PR branch reference file.");
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: prRepoOwner,
|
||||
repo: prRepoName,
|
||||
path: "app/core/src/main/resources/messages_en_GB.properties",
|
||||
path: "stirling-pdf/src/main/resources/messages_en_GB.properties",
|
||||
ref: branch,
|
||||
});
|
||||
|
||||
@@ -186,7 +174,7 @@ jobs:
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
path: "app/core/src/main/resources/messages_en_GB.properties",
|
||||
path: "stirling-pdf/src/main/resources/messages_en_GB.properties",
|
||||
ref: "main",
|
||||
});
|
||||
|
||||
@@ -289,4 +277,4 @@ jobs:
|
||||
rm -rf pr-branch
|
||||
rm -f pr-branch-messages_en_GB.properties main-branch-messages_en_GB.properties changed_files.txt result.txt
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
name: Auto V2 Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- deploy-on-v2-commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-v2-on-push:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
chmod 600 ../private.key
|
||||
|
||||
|
||||
- name: Deploy to VPS on port 3000
|
||||
run: |
|
||||
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||
|
||||
cat > $UNIQUE_NAME << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
container_name: stirling-v2-backend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "13000:8080"
|
||||
volumes:
|
||||
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2/config:/configs:rw
|
||||
- /stirling/V2/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
|
||||
baseUrl: "https://demo.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
container_name: stirling-v2-frontend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Copy to remote with unique name
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||
|
||||
# SSH and rename/move atomically to avoid interference
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
mkdir -p /stirling/V2/{data,config,logs}
|
||||
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||
cd /stirling/V2
|
||||
docker-compose down || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker system prune -af --volumes || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
name: Frontend License Report Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
pull_request:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate-frontend-license-report:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
env:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Generate frontend license report (internal PR)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
working-directory: frontend
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: npm run generate-licenses
|
||||
|
||||
- name: Generate frontend license report (fork PRs, pinned)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
env:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p src/assets
|
||||
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Postprocess with project script (BASE version)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
env:
|
||||
PR_IS_FORK: "true"
|
||||
run: |
|
||||
node base/frontend/scripts/generate-licenses.js \
|
||||
--input frontend/src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Copy postprocessed artifacts back (fork PRs)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
run: |
|
||||
mkdir -p frontend/src/assets
|
||||
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
|
||||
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
|
||||
fi
|
||||
if [ -f "base/frontend/src/assets/license-warnings.json" ]; then
|
||||
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
|
||||
fi
|
||||
|
||||
- name: Check for license warnings
|
||||
run: |
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get all comments on the PR
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter for license check comments
|
||||
const licenseComments = comments.filter(comment =>
|
||||
comment.body.includes('## ✅ Frontend License Check Passed') ||
|
||||
comment.body.includes('## ❌ Frontend License Check Failed')
|
||||
);
|
||||
|
||||
// Delete old license check comments
|
||||
for (const comment of licenseComments) {
|
||||
console.log(`Deleting old license check comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
- name: Summarize results (fork PRs)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
run: |
|
||||
{
|
||||
echo "## Frontend License Check"
|
||||
echo ""
|
||||
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
|
||||
echo "❌ **Failed** – incompatible or unknown licenses found."
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo ""
|
||||
echo "### Warnings"
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
|
||||
fi
|
||||
else
|
||||
echo "✅ **Passed** – no license warnings detected."
|
||||
fi
|
||||
echo ""
|
||||
echo "_Note: This is a fork PR. PR comments are disabled; use this summary._"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||
|
||||
let commentBody;
|
||||
|
||||
if (hasWarnings) {
|
||||
// Read warnings file to get specific issues
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||
} catch (e) {
|
||||
warningDetails = 'Unable to read warning details';
|
||||
}
|
||||
|
||||
commentBody = `## ❌ Frontend License Check Failed
|
||||
|
||||
The frontend license check has detected compatibility warnings that require review:
|
||||
|
||||
${warningDetails}
|
||||
|
||||
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||
|
||||
_This check will fail the PR until license issues are resolved._`;
|
||||
} else {
|
||||
commentBody = `## ✅ Frontend License Check Passed
|
||||
|
||||
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||
|
||||
The frontend license report has been updated successfully.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
- name: Fail workflow if license warnings exist (PR only)
|
||||
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: |
|
||||
echo "❌ License warnings detected. Failing the workflow."
|
||||
exit 1
|
||||
|
||||
# Push Event: Commit license files and create PR
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
# Note: Do NOT commit license-warnings.json - it's only for PR review
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare PR body (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||
|
||||
This PR updates the frontend license report based on changes to package.json dependencies."
|
||||
|
||||
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
|
||||
## ⚠️ License Compatibility Warnings
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
|
||||
Please review these licenses to ensure they are acceptable for your use case."
|
||||
fi
|
||||
|
||||
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$PR_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request (Push only)
|
||||
id: cpr
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Frontend 3rd Party Licenses"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: update-frontend-3rd-party-licenses
|
||||
base: V2
|
||||
title: "Update Frontend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,frontend
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
- name: Enable Pull Request Automerge (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Add review required label (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -7,18 +7,6 @@ on:
|
||||
paths:
|
||||
- "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:
|
||||
contents: read
|
||||
|
||||
@@ -31,12 +19,12 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -51,10 +39,10 @@ jobs:
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
distribution: "adopt"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
@@ -69,11 +57,11 @@ jobs:
|
||||
|
||||
- name: Move and rename license file
|
||||
run: |
|
||||
mv build/reports/dependency-license/index.json app/core/src/main/resources/static/3rdPartyLicenses.json
|
||||
mv build/reports/dependency-license/index.json stirling-pdf/src/main/resources/static/3rdPartyLicenses.json
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git add app/core/src/main/resources/static/3rdPartyLicenses.json
|
||||
git add stirling-pdf/src/main/resources/static/3rdPartyLicenses.json
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
|
||||
@@ -21,11 +21,11 @@ jobs:
|
||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -60,11 +60,11 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
run: |
|
||||
mkdir ./binaries
|
||||
mv ./build/launch4j/Stirling-PDF.exe ./binaries/win-Stirling-PDF-portable-Server${{ matrix.file_suffix }}.exe
|
||||
mv ./app/core/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
mv ./stirling-pdf/build/libs/stirling-pdf-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
@@ -110,12 +110,12 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: stirling-${{ matrix.file_suffix }}binaries
|
||||
|
||||
@@ -148,11 +148,11 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -238,12 +238,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: ${{ matrix.platform }}binaries
|
||||
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
|
||||
- name: Install Cosign
|
||||
if: matrix.os == 'windows-latest'
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
|
||||
- name: Generate key pair
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -301,12 +301,12 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
- name: Upload binaries, attestations and signatures to Release and create GitHub Release
|
||||
|
||||
@@ -2,9 +2,8 @@ name: Pre-commit
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -17,12 +16,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -38,7 +37,6 @@ jobs:
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: 'pip' # caching pip dependencies
|
||||
cache-dependency-path: ./.github/scripts/requirements_pre_commit.txt
|
||||
|
||||
- name: Run Pre-Commit Hooks
|
||||
run: |
|
||||
@@ -47,15 +45,6 @@ jobs:
|
||||
- run: pre-commit run --all-files -c .pre-commit-config.yaml
|
||||
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
|
||||
run: |
|
||||
git add .
|
||||
|
||||
@@ -7,18 +7,6 @@ on:
|
||||
- master
|
||||
- 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:
|
||||
contents: read
|
||||
|
||||
@@ -30,11 +18,11 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -42,7 +30,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -54,7 +42,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
@@ -67,13 +55,13 @@ jobs:
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -88,7 +76,7 @@ jobs:
|
||||
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
@@ -134,7 +122,7 @@ jobs:
|
||||
|
||||
- name: Generate tags ultra-lite
|
||||
id: meta2
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
@@ -165,7 +153,7 @@ jobs:
|
||||
|
||||
- name: Generate tags fat
|
||||
id: meta3
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
|
||||
@@ -23,11 +23,11 @@ jobs:
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
@@ -83,19 +83,19 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: binaries${{ matrix.file_suffix }}
|
||||
- name: Display structure of downloaded files
|
||||
run: ls -R
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
|
||||
|
||||
- name: Generate key pair
|
||||
run: cosign generate-key-pair
|
||||
@@ -161,12 +161,12 @@ jobs:
|
||||
file_suffix: ""
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download signed artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: signed${{ matrix.file_suffix }}
|
||||
|
||||
|
||||
@@ -34,12 +34,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -74,6 +74,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -9,18 +9,6 @@ on:
|
||||
- main
|
||||
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:
|
||||
pull-requests: read
|
||||
actions: read
|
||||
@@ -30,16 +18,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Build and analyze with Gradle
|
||||
env:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -6,18 +6,6 @@ on:
|
||||
branches:
|
||||
- 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:
|
||||
contents: read
|
||||
|
||||
@@ -26,11 +14,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -38,10 +26,10 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
- uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
|
||||
- name: Generate Swagger documentation
|
||||
run: ./gradlew :stirling-pdf:generateOpenApiDocs
|
||||
run: ./gradlew generateOpenApiDocs
|
||||
|
||||
- name: Upload Swagger Documentation to SwaggerHub
|
||||
run: ./gradlew swaggerhubUpload
|
||||
|
||||
@@ -8,22 +8,10 @@ on:
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "app/core/src/main/resources/messages_*.properties"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "stirling-pdf/src/main/resources/messages_*.properties"
|
||||
- "stirling-pdf/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "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:
|
||||
contents: read
|
||||
|
||||
@@ -32,11 +20,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -53,11 +41,11 @@ jobs:
|
||||
|
||||
- name: Sync translation property files
|
||||
run: |
|
||||
python .github/scripts/check_language_properties.py --reference-file "app/core/src/main/resources/messages_en_GB.properties" --branch main
|
||||
python .github/scripts/check_language_properties.py --reference-file "stirling-pdf/src/main/resources/messages_en_GB.properties" --branch main
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add app/core/src/main/resources/messages_*.properties
|
||||
git add stirling-pdf/src/main/resources/messages_*.properties
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -113,4 +101,4 @@ jobs:
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
app/core/src/main/resources/messages_*.properties
|
||||
stirling-pdf/src/main/resources/messages_*.properties
|
||||
|
||||
@@ -4,18 +4,6 @@ on:
|
||||
push:
|
||||
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:
|
||||
contents: read
|
||||
|
||||
@@ -24,12 +12,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
@@ -37,11 +25,6 @@ jobs:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
@@ -57,7 +40,7 @@ jobs:
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
@@ -122,16 +105,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run TestDriver.ai
|
||||
uses: testdriverai/action@f0d0f45fdd684db628baa843fe9313f3ca3a8aa8 #1.1.3
|
||||
@@ -156,7 +134,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
+5
-15
@@ -27,7 +27,7 @@ clientWebUI/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/stirling/
|
||||
stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
@@ -125,13 +125,10 @@ SwaggerDoc.json
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.db
|
||||
build
|
||||
app/core/build
|
||||
app/common/build
|
||||
app/proprietary/build
|
||||
common/build
|
||||
proprietary/build
|
||||
stirling-pdf/build
|
||||
/build
|
||||
/stirling-pdf/build
|
||||
/common/build
|
||||
/proprietary/build
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
@@ -203,10 +200,3 @@ id_ed25519.pub
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
|
||||
# Translation temp files
|
||||
*_compact.json
|
||||
*compact*.json
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
|
||||
+5
-15
@@ -1,15 +1,15 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.7
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- --line-length=127
|
||||
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||
files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$
|
||||
exclude: (split_photos.py)
|
||||
- id: ruff-format
|
||||
files: ^((\.github/scripts|scripts|app/core/src/main/resources/static/python)/.+)?[^/]+\.py$
|
||||
files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$
|
||||
exclude: (split_photos.py)
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.4.1
|
||||
@@ -20,9 +20,9 @@ repos:
|
||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||
- --quiet-level=2
|
||||
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|stirling-pdf/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||
- repo: https://github.com/gitleaks/gitleaks
|
||||
rev: v8.28.0
|
||||
rev: v8.27.2
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
@@ -34,13 +34,3 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js|\.github/workflows/.*$)
|
||||
# - repo: https://github.com/thibaudcolas/pre-commit-stylelint
|
||||
# rev: v16.21.1
|
||||
# hooks:
|
||||
# - id: stylelint
|
||||
# additional_dependencies:
|
||||
# - stylelint@16.21.1
|
||||
# - stylelint-config-standard@38.0.0
|
||||
# - "@stylistic/stylelint-plugin@3.1.3"
|
||||
# files: \.(css)$
|
||||
# args: [--fix]
|
||||
|
||||
Vendored
-3
@@ -17,8 +17,5 @@
|
||||
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
|
||||
"charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+11
-30
@@ -2,20 +2,13 @@
|
||||
"editor.wordSegmenterLocales": "",
|
||||
"editor.guides.bracketPairs": "active",
|
||||
"editor.guides.bracketPairsHorizontal": "active",
|
||||
"editor.defaultFormatter": "EditorConfig.EditorConfig",
|
||||
"cSpell.enabled": false,
|
||||
"[feature]": {
|
||||
"editor.defaultFormatter": "alexkrechik.cucumberautocomplete"
|
||||
},
|
||||
"[java]": {
|
||||
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "stylelint.vscode-stylelint"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
@@ -34,14 +27,11 @@
|
||||
"[gradle]": {
|
||||
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.format.enabled": true,
|
||||
"java.format.settings.profile": "GoogleStyle",
|
||||
"java.format.settings.google.version": "1.28.0",
|
||||
"java.format.settings.google.version": "1.27.0",
|
||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
||||
// (DE) Aktiviert Kommentare im Java-Format.
|
||||
// (EN) Enables comments in Java formatting.
|
||||
@@ -80,17 +70,13 @@
|
||||
".venv*/",
|
||||
".vscode/",
|
||||
"bin/",
|
||||
"app/core/bin/",
|
||||
"app/common/bin/",
|
||||
"app/proprietary/bin/",
|
||||
"common/bin/",
|
||||
"proprietary/bin/",
|
||||
"build/",
|
||||
"app/core/build/",
|
||||
"app/common/build/",
|
||||
"app/proprietary/build/",
|
||||
"common/build/",
|
||||
"proprietary/build/",
|
||||
"configs/",
|
||||
"app/core/configs/",
|
||||
"customFiles/",
|
||||
"app/core/customFiles/",
|
||||
"docs/",
|
||||
"exampleYmlFiles",
|
||||
"gradle/",
|
||||
@@ -102,9 +88,8 @@
|
||||
".git-blame-ignore-revs",
|
||||
".gitattributes",
|
||||
".gitignore",
|
||||
"app/core/.gitignore",
|
||||
"app/common/.gitignore",
|
||||
"app/proprietary/.gitignore",
|
||||
"common/.gitignore",
|
||||
"proprietary/.gitignore",
|
||||
".pre-commit-config.yaml",
|
||||
],
|
||||
// Enables signature help in Java.
|
||||
@@ -134,13 +119,9 @@
|
||||
"html.format.indentHandlebars": true,
|
||||
"html.format.preserveNewLines": true,
|
||||
"html.format.maxPreserveNewLines": 2,
|
||||
"stylelint.configFile": "devTools/.stylelintrc.json",
|
||||
"java.project.sourcePaths": [
|
||||
"app/core/src/main/java",
|
||||
"app/common/src/main/java",
|
||||
"app/proprietary/src/main/java"
|
||||
],
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
"stirling-pdf/src/main/java",
|
||||
"common/src/main/java",
|
||||
"proprietary/src/main/java"
|
||||
]
|
||||
}
|
||||
|
||||
-300
@@ -1,300 +0,0 @@
|
||||
# Adding New React Tools to Stirling PDF
|
||||
|
||||
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
|
||||
|
||||
## Overview
|
||||
|
||||
When adding tools, follow this systematic approach using the established patterns and architecture.
|
||||
|
||||
## 1. Create Tool Structure
|
||||
|
||||
Create these files in the correct directories:
|
||||
```
|
||||
frontend/src/hooks/tools/[toolName]/
|
||||
├── use[ToolName]Parameters.ts # Parameter definitions and validation
|
||||
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
|
||||
|
||||
frontend/src/components/tools/[toolName]/
|
||||
└── [ToolName]Settings.tsx # Settings UI component (if needed)
|
||||
|
||||
frontend/src/tools/
|
||||
└── [ToolName].tsx # Main tool component
|
||||
```
|
||||
|
||||
## 2. Implementation Pattern
|
||||
|
||||
Use `useBaseTool` for simplified hook management. This is the recommended approach for all new tools:
|
||||
|
||||
**Parameters Hook** (`use[ToolName]Parameters.ts`):
|
||||
```typescript
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface [ToolName]Parameters extends BaseParameters {
|
||||
// Define your tool-specific parameters here
|
||||
someOption: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: [ToolName]Parameters = {
|
||||
someOption: false,
|
||||
};
|
||||
|
||||
export const use[ToolName]Parameters = (): BaseParametersHook<[ToolName]Parameters> => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'your-endpoint-name',
|
||||
validateFn: (params) => true, // Add validation logic
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Operation Hook** (`use[ToolName]Operation.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
|
||||
export const build[ToolName]FormData = (parameters: [ToolName]Parameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
// Add parameters to formData
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const [toolName]OperationConfig = {
|
||||
toolType: ToolType.singleFile, // or ToolType.multiFile (buildFormData's file parameter will need to be updated)
|
||||
buildFormData: build[ToolName]FormData,
|
||||
operationType: '[toolName]',
|
||||
endpoint: '/api/v1/category/endpoint-name',
|
||||
filePrefix: 'processed_', // Will be overridden with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const use[ToolName]Operation = () => {
|
||||
const { t } = useTranslation();
|
||||
return useToolOperation({
|
||||
...[toolName]OperationConfig,
|
||||
filePrefix: t('[toolName].filenamePrefix', 'processed') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('[toolName].error.failed', 'Operation failed'))
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Main Component** (`[ToolName].tsx`):
|
||||
```typescript
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import { use[ToolName]Parameters } from "../hooks/tools/[toolName]/use[ToolName]Parameters";
|
||||
import { use[ToolName]Operation } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const base = useBaseTool('[toolName]', use[ToolName]Parameters, use[ToolName]Operation, props);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("[toolName].files.placeholder", "Select files to get started"),
|
||||
},
|
||||
steps: [
|
||||
// Add settings steps if needed
|
||||
],
|
||||
executeButton: {
|
||||
text: t("[toolName].submit", "Process"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("[toolName].results.title", "Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
[ToolName].tool = () => use[ToolName]Operation;
|
||||
export default [ToolName] as ToolComponent;
|
||||
```
|
||||
|
||||
**Note**: Some existing tools (like AddPassword, Compress) use a legacy pattern with manual hook management. **Always use the Modern Pattern above for new tools** - it's cleaner, more maintainable, and includes automation support.
|
||||
|
||||
## 3. Register Tool in System
|
||||
Update these files to register your new tool:
|
||||
|
||||
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
|
||||
1. Add imports at the top:
|
||||
```typescript
|
||||
import [ToolName] from "../tools/[ToolName]";
|
||||
import { [toolName]OperationConfig } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||
import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Settings";
|
||||
```
|
||||
|
||||
2. Add tool entry in the `allTools` object:
|
||||
```typescript
|
||||
[toolName]: {
|
||||
icon: <LocalIcon icon="your-icon-name" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.[toolName].title", "Tool Name"),
|
||||
component: [ToolName],
|
||||
description: t("home.[toolName].desc", "Tool description"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS, // or appropriate category
|
||||
subcategoryId: SubcategoryId.APPROPRIATE_SUBCATEGORY,
|
||||
maxFiles: -1, // or specific number
|
||||
endpoints: ["endpoint-name"],
|
||||
operationConfig: [toolName]OperationConfig,
|
||||
settingsComponent: [ToolName]Settings, // if settings exist
|
||||
},
|
||||
```
|
||||
|
||||
## 4. Add Tooltips (Optional but Recommended)
|
||||
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
|
||||
|
||||
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const use[ToolName]Tips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("[toolName].tooltip.header.title", "Tool Overview")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("[toolName].tooltip.description.title", "What does this tool do?"),
|
||||
description: t("[toolName].tooltip.description.text", "Simple explanation in everyday language that non-technical users can understand."),
|
||||
bullets: [
|
||||
t("[toolName].tooltip.description.bullet1", "Easy-to-understand benefit 1"),
|
||||
t("[toolName].tooltip.description.bullet2", "Easy-to-understand benefit 2")
|
||||
]
|
||||
}
|
||||
// Add more tip sections as needed
|
||||
]
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Add tooltip to your main component:**
|
||||
```typescript
|
||||
import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const tips = use[ToolName]Tips();
|
||||
|
||||
// In your steps array:
|
||||
steps: [
|
||||
{
|
||||
title: t("[toolName].steps.settings", "Settings"),
|
||||
tooltip: tips, // Add this line
|
||||
content: <[ToolName]Settings ... />
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 5. Add Translations
|
||||
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
|
||||
|
||||
**File to update:** `frontend/public/locales/en-GB/translation.json`
|
||||
|
||||
**Required Translation Keys**:
|
||||
```json
|
||||
{
|
||||
"home": {
|
||||
"[toolName]": {
|
||||
"title": "Tool Name",
|
||||
"desc": "Tool description"
|
||||
}
|
||||
},
|
||||
"[toolName]": {
|
||||
"title": "Tool Name",
|
||||
"submit": "Process",
|
||||
"filenamePrefix": "processed",
|
||||
"files": {
|
||||
"placeholder": "Select files to get started"
|
||||
},
|
||||
"steps": {
|
||||
"settings": "Settings"
|
||||
},
|
||||
"options": {
|
||||
"title": "Tool Options",
|
||||
"someOption": "Option Label",
|
||||
"someOption.desc": "Option description",
|
||||
"note": "General information about the tool."
|
||||
},
|
||||
"results": {
|
||||
"title": "Results"
|
||||
},
|
||||
"error": {
|
||||
"failed": "Operation failed"
|
||||
},
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "Tool Overview"
|
||||
},
|
||||
"description": {
|
||||
"title": "What does this tool do?",
|
||||
"text": "Simple explanation in everyday language",
|
||||
"bullet1": "Easy-to-understand benefit 1",
|
||||
"bullet2": "Easy-to-understand benefit 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Translation Notes:**
|
||||
- **Only update `en-GB/translation.json`** - other locale files are managed separately
|
||||
- Use descriptive keys that match your component's `t()` calls
|
||||
- Include tooltip translations if you created tooltip hooks
|
||||
- Add `options.*` keys if your tool has settings with descriptions
|
||||
|
||||
**Tooltip Writing Guidelines:**
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Focus on benefits** - explain what the user gains, not how it works internally
|
||||
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
|
||||
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
|
||||
- **Keep descriptions concise** - 1-2 sentences maximum per section
|
||||
- **Use bullet points** for multiple benefits or features
|
||||
|
||||
## 6. Migration from Thymeleaf
|
||||
When migrating existing Thymeleaf templates:
|
||||
|
||||
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
|
||||
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
|
||||
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
|
||||
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
|
||||
|
||||
## 7. Testing Your Tool
|
||||
- Verify tool appears in UI with correct icon and description
|
||||
- Test with various file sizes and types
|
||||
- Confirm translations work
|
||||
- Check error handling
|
||||
- Test undo functionality
|
||||
- Verify results display correctly
|
||||
|
||||
## Tool Development Patterns
|
||||
|
||||
### Three Tool Patterns:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
@@ -25,113 +25,23 @@ Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security featur
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: DO NOT run npm install commands - package management handled separately
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `npm run tauri-build` (native desktop application)
|
||||
- **Web Server**: `npm run build` then serve dist/ folder
|
||||
- **Development**: `npm run tauri-dev` for desktop dev mode
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `src/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
});
|
||||
#### Tailwind CSS Setup (if not already installed)
|
||||
```bash
|
||||
cd frontend
|
||||
npm install -D tailwindcss postcss autoprefixer
|
||||
npx tailwindcss init -p
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_',
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (replacing legacy Thymeleaf templates)
|
||||
- **Current Status**: Active development to replace Thymeleaf UI with modern React SPA
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **URL Parameters**: Deep linking support for tool states and configurations
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
@@ -149,8 +59,9 @@ return useToolOperation({
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Component Architecture
|
||||
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
### Template System (Legacy + Modern)
|
||||
- **Legacy Thymeleaf Templates**: Located in `src/main/resources/templates/` (being phased out)
|
||||
- **Modern React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
@@ -180,17 +91,13 @@ return useToolOperation({
|
||||
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
|
||||
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
## Frontend Migration Notes
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
- **Current Branch**: `feature/react-overhaul` - Active React SPA development
|
||||
- **Migration Status**: Core tools (Split, Merge, Compress) converted to React with URL parameter support
|
||||
- **File Management**: Implemented IndexedDB storage with thumbnail generation using PDF.js
|
||||
- **Tools Architecture**: Each tool receives `params` and `updateParams` for URL state synchronization
|
||||
- **Remaining Work**: Convert remaining Thymeleaf templates to React components
|
||||
|
||||
## Important Notes
|
||||
|
||||
@@ -201,12 +108,6 @@ return useToolOperation({
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
|
||||
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
|
||||
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
|
||||
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
|
||||
@@ -4,8 +4,8 @@ Copyright (c) 2025 Stirling PDF Inc.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "app/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
|
||||
* All content that resides under the "proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "proprietary/LICENSE".
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the MIT License as defined below.
|
||||
|
||||
|
||||
@@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Account Security API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/account"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/account")
|
||||
@Tag(
|
||||
name = "Account Security",
|
||||
description =
|
||||
"""
|
||||
Account security and protection services for user safety and organizational compliance.
|
||||
|
||||
This endpoint group provides account security capabilities for organizations requiring
|
||||
enhanced protection against unauthorized access, security threats, and compliance violations.
|
||||
|
||||
Common use cases:
|
||||
• Corporate security policy compliance and SOX, HIPAA, GDPR requirements
|
||||
• Fraud prevention, identity theft protection, and account compromise recovery
|
||||
• Multi-factor authentication implementation and insider threat mitigation
|
||||
• Account recovery and emergency access procedures
|
||||
|
||||
Business applications:
|
||||
• Enterprise risk management, security governance, and customer trust protection
|
||||
• Legal liability reduction and insurance requirement fulfillment
|
||||
• Audit preparation, compliance reporting, and business continuity management
|
||||
|
||||
Operational scenarios:
|
||||
• Security incident response, forensic investigation, and user training
|
||||
• Emergency account lockdown, suspicious activity monitoring, and compliance documentation
|
||||
|
||||
Target users: Security administrators, compliance officers, and organizations
|
||||
prioritizing account security and regulatory compliance.
|
||||
""")
|
||||
public @interface AccountSecurityApi {}
|
||||
@@ -1,48 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Admin Settings API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/settings"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/settings")
|
||||
@Tag(
|
||||
name = "Admin Settings",
|
||||
description =
|
||||
"""
|
||||
System administration and configuration management for enterprise deployments.
|
||||
|
||||
This endpoint group provides administrative control for organizations deploying
|
||||
Stirling PDF in production environments with multi-user scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Enterprise deployment configuration and multi-tenant environment management
|
||||
• Security policy enforcement, compliance monitoring, and capacity planning
|
||||
• Operational maintenance, troubleshooting, and enterprise infrastructure integration
|
||||
• Disaster recovery and business continuity preparation
|
||||
|
||||
Business applications:
|
||||
• Corporate IT governance, policy enforcement, and compliance reporting
|
||||
• Cost optimization, SLA monitoring, and vendor management oversight
|
||||
• Risk management and security incident response
|
||||
|
||||
Operational scenarios:
|
||||
• 24/7 production monitoring, scheduled maintenance, and system updates
|
||||
• Emergency response, change management, and performance optimization
|
||||
|
||||
Target users: IT administrators, system engineers, and operations teams
|
||||
responsible for enterprise-grade document processing infrastructure.
|
||||
""")
|
||||
public @interface AdminApi {}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Admin Server Certificate API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/server-certificate"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/server-certificate")
|
||||
@Tag(
|
||||
name = "Admin - Server Certificate",
|
||||
description =
|
||||
"""
|
||||
Server certificate management for secure enterprise deployments and encrypted communications.
|
||||
|
||||
This endpoint group provides certificate lifecycle management for organizations
|
||||
requiring secure communications in document processing infrastructure.
|
||||
|
||||
Common use cases:
|
||||
• Corporate security compliance and encrypted communications for healthcare/finance
|
||||
• Customer data protection, internal audits, and multi-environment standardization
|
||||
• Third-party security assessments and disaster recovery security measures
|
||||
|
||||
Business applications:
|
||||
• Enterprise security governance, client trust protection, and secure B2B exchange
|
||||
• Legal requirement fulfillment, liability reduction, and M&A security preparation
|
||||
|
||||
Operational scenarios:
|
||||
• Certificate renewal, emergency replacement, and security incident response
|
||||
• Multi-site deployment coordination and cloud migration preparation
|
||||
|
||||
Target users: Security administrators, compliance officers, and IT infrastructure
|
||||
teams requiring enterprise-grade security for document processing systems.
|
||||
""")
|
||||
public @interface AdminServerCertificateApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Analysis API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/analysis"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/analysis")
|
||||
@Tag(
|
||||
name = "Analysis",
|
||||
description =
|
||||
"""
|
||||
Document analysis and information extraction services for content intelligence and insights.
|
||||
|
||||
This endpoint group provides analytical capabilities to understand document structure,
|
||||
extract information, and generate insights from PDF content for automated processing.
|
||||
|
||||
Common use cases:
|
||||
• Document inventory management and content audit for compliance verification
|
||||
• Quality assurance workflows and business intelligence analytics
|
||||
• Migration planning, accessibility evaluation, and document forensics
|
||||
|
||||
Business applications:
|
||||
• Legal discovery, financial document review, and healthcare records analysis
|
||||
• Academic research, government processing, and publishing optimization
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale profiling, migration assessment, and performance optimization
|
||||
• Automated quality control and content strategy development
|
||||
|
||||
Target users: Data analysts, QA teams, administrators, and business intelligence
|
||||
professionals requiring detailed document insights.
|
||||
""")
|
||||
public @interface AnalysisApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Configuration API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/config"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/config")
|
||||
@Tag(
|
||||
name = "Config",
|
||||
description =
|
||||
"""
|
||||
System configuration management for deployment optimization and operational control.
|
||||
|
||||
This endpoint group provides system configuration capabilities for organizations
|
||||
deploying and operating Stirling PDF in various environments.
|
||||
|
||||
Common use cases:
|
||||
• Environment-specific deployment and performance tuning for varying workloads
|
||||
• Resource optimization, cost management, and infrastructure integration
|
||||
• Compliance configuration, disaster recovery, and multi-environment standardization
|
||||
|
||||
Business applications:
|
||||
• Operational cost optimization, SLA compliance, and risk management
|
||||
• Vendor integration, change management, and capacity planning
|
||||
|
||||
Operational scenarios:
|
||||
• System deployment, performance troubleshooting, and emergency changes
|
||||
• Planned maintenance and multi-site deployment coordination
|
||||
|
||||
Target users: System administrators, DevOps engineers, and IT operations teams
|
||||
responsible for deployment configuration and system optimization.
|
||||
""")
|
||||
public @interface ConfigApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Convert API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/convert"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(
|
||||
name = "Convert",
|
||||
description =
|
||||
"""
|
||||
Document format transformation services for cross-platform compatibility and workflow integration.
|
||||
|
||||
This endpoint group enables transformation between various formats, supporting
|
||||
diverse business workflows and system integrations for mixed document ecosystems.
|
||||
|
||||
Common use cases:
|
||||
• Legacy system integration, document migration, and cross-platform sharing
|
||||
• Archive standardization, publishing preparation, and content adaptation
|
||||
• Accessibility compliance and mobile-friendly document preparation
|
||||
|
||||
Business applications:
|
||||
• Enterprise content management, digital publishing, and educational platforms
|
||||
• Legal document processing, healthcare interoperability, and government standardization
|
||||
|
||||
Integration scenarios:
|
||||
• API-driven pipelines, automated workflow preparation, and batch conversions
|
||||
• Real-time format adaptation for user requests
|
||||
|
||||
Target users: System integrators, content managers, digital archivists, and
|
||||
organizations requiring flexible document format interoperability.
|
||||
""")
|
||||
public @interface ConvertApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Database Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/database"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/database")
|
||||
@Tag(
|
||||
name = "Database",
|
||||
description =
|
||||
"""
|
||||
Database operations for data protection and business continuity management.
|
||||
|
||||
This endpoint group provides essential database operations for organizations requiring
|
||||
reliable data protection and recovery capabilities.
|
||||
|
||||
Common use cases:
|
||||
• Regular data backup, disaster recovery, and business continuity planning
|
||||
• System migration, compliance management, and development environment support
|
||||
• Operational troubleshooting and scheduled maintenance operations
|
||||
|
||||
Business applications:
|
||||
• Risk management, regulatory compliance, and operational resilience
|
||||
• Data governance, change management, and quality assurance support
|
||||
|
||||
Operational scenarios:
|
||||
• Routine backup, emergency recovery, and system maintenance preparation
|
||||
• Data migration projects and performance monitoring
|
||||
|
||||
Target users: Operations teams, system administrators, and organizations requiring
|
||||
reliable data protection and operational database management.
|
||||
""")
|
||||
public @interface DatabaseApi {}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Database Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/database"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/database")
|
||||
@Tag(
|
||||
name = "Database Management",
|
||||
description =
|
||||
"""
|
||||
Enterprise database administration for production data management and business continuity.
|
||||
|
||||
This endpoint group provides database administration capabilities for organizations
|
||||
operating Stirling PDF in production environments.
|
||||
|
||||
Common use cases:
|
||||
• Business continuity, disaster recovery, and regulatory compliance requirements
|
||||
• Performance optimization, data security, and system migration projects
|
||||
• Audit preparation, compliance reporting, and cost optimization
|
||||
|
||||
Business applications:
|
||||
• Enterprise risk management, regulatory compliance, and SLA monitoring
|
||||
• Data retention policies, security incident response, and vendor oversight
|
||||
|
||||
Operational scenarios:
|
||||
• Scheduled maintenance, emergency recovery, and capacity planning
|
||||
• Performance troubleshooting and multi-environment deployment coordination
|
||||
|
||||
Target users: Database administrators, IT operations teams, and enterprise
|
||||
administrators responsible for production data management and system reliability.
|
||||
""")
|
||||
public @interface DatabaseManagementApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Filter API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/filter"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/filter")
|
||||
@Tag(
|
||||
name = "Filter",
|
||||
description =
|
||||
"""
|
||||
Document content filtering and search operations for information discovery and organization.
|
||||
|
||||
This endpoint group enables intelligent content discovery and organization within
|
||||
document collections for content-based processing and information extraction.
|
||||
|
||||
Common use cases:
|
||||
• Legal discovery, research organization, and compliance auditing
|
||||
• Content moderation, academic research, and business intelligence
|
||||
• Quality assurance and content validation workflows
|
||||
|
||||
Business applications:
|
||||
• Contract analysis, financial review, and healthcare records organization
|
||||
• Government processing, educational curation, and IP protection
|
||||
|
||||
Workflow scenarios:
|
||||
• Large-scale processing, automated classification, and information extraction
|
||||
• Document preparation for further processing or analysis
|
||||
|
||||
Target users: Legal professionals, researchers, compliance officers, and
|
||||
organizations requiring intelligent document content discovery and organization.
|
||||
""")
|
||||
public @interface FilterApi {}
|
||||
@@ -1,42 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for General PDF processing API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/general"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(
|
||||
name = "General",
|
||||
description =
|
||||
"""
|
||||
Core PDF processing operations for fundamental document manipulation workflows.
|
||||
|
||||
This endpoint group provides essential PDF functionality that forms the foundation
|
||||
of most document processing workflows across various industries.
|
||||
|
||||
Common use cases:
|
||||
• Document preparation for archival systems and content organization
|
||||
• File preparation for distribution, accessibility compliance, and batch processing
|
||||
• Document consolidation for reporting and legal compliance workflows
|
||||
|
||||
Typical applications:
|
||||
• Content management, publishing workflows, and educational content distribution
|
||||
• Business process automation and archive management
|
||||
|
||||
Target users: Content managers, document processors, and organizations requiring
|
||||
reliable foundational PDF manipulation capabilities.
|
||||
""")
|
||||
public @interface GeneralApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Info API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/info"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/info")
|
||||
@Tag(
|
||||
name = "Info",
|
||||
description =
|
||||
"""
|
||||
System information and operational insights for monitoring and performance management.
|
||||
|
||||
This endpoint group provides system information and operational metrics for organizations
|
||||
operating Stirling PDF in production environments.
|
||||
|
||||
Common use cases:
|
||||
• System health monitoring, performance optimization, and capacity planning
|
||||
• Troubleshooting, compliance monitoring, and SLA reporting
|
||||
• Cost optimization, usage analysis, and security monitoring
|
||||
|
||||
Business applications:
|
||||
• Operational cost management, business continuity monitoring, and vendor management
|
||||
• Compliance reporting, strategic planning, and customer service tracking
|
||||
|
||||
Operational scenarios:
|
||||
• 24/7 monitoring, scheduled maintenance, and emergency response coordination
|
||||
• System upgrade planning and capacity scaling decisions
|
||||
|
||||
Target users: Operations teams, system administrators, and management teams requiring
|
||||
operational insights and system performance visibility.
|
||||
""")
|
||||
public @interface InfoApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Miscellaneous API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/misc"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(
|
||||
name = "Misc",
|
||||
description =
|
||||
"""
|
||||
Specialized utilities and supplementary tools for enhanced document processing workflows.
|
||||
|
||||
This endpoint group provides utility operations that support core document processing
|
||||
tasks and address specific workflow needs in real-world scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Document optimization for bandwidth-limited environments and storage cost management
|
||||
• Document repair, content extraction, and validation for quality assurance
|
||||
• Accessibility improvement and custom processing for specialized needs
|
||||
|
||||
Business applications:
|
||||
• Web publishing optimization, email attachment management, and archive efficiency
|
||||
• Mobile compatibility, print production, and legacy document recovery
|
||||
|
||||
Operational scenarios:
|
||||
• Batch processing, quality control, and performance optimization
|
||||
• Troubleshooting and recovery of problematic documents
|
||||
|
||||
Target users: System administrators, document specialists, and organizations requiring
|
||||
specialized document processing and optimization tools.
|
||||
""")
|
||||
public @interface MiscApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Pipeline API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/pipeline"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pipeline")
|
||||
@Tag(
|
||||
name = "Pipeline",
|
||||
description =
|
||||
"""
|
||||
Automated document processing workflows for complex multi-stage business operations.
|
||||
|
||||
This endpoint group enables organizations to create sophisticated document processing
|
||||
workflows that combine multiple operations into streamlined, repeatable processes.
|
||||
|
||||
Common use cases:
|
||||
• Invoice processing, legal document review, and healthcare records standardization
|
||||
• Government processing, educational content preparation, and publishing automation
|
||||
• Contract lifecycle management and approval processes
|
||||
|
||||
Business applications:
|
||||
• Automated compliance reporting, large-scale migration, and quality assurance
|
||||
• Archive preparation, content delivery, and document approval workflows
|
||||
|
||||
Operational scenarios:
|
||||
• Scheduled batch processing and event-driven document processing
|
||||
• Multi-department coordination and business system integration
|
||||
|
||||
Target users: Business process managers, IT automation specialists, and organizations
|
||||
requiring consistent, repeatable document processing workflows.
|
||||
""")
|
||||
public @interface PipelineApi {}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Proprietary UI Data API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/proprietary/ui-data"), and OpenAPI @Tag. Note:
|
||||
* Controllers using this annotation should also add @EnterpriseEndpoint.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||
@Tag(
|
||||
name = "Proprietary UI Data",
|
||||
description =
|
||||
"""
|
||||
Enterprise user interface data services for commercial deployments and advanced business features.
|
||||
|
||||
This endpoint group provides enhanced data services for commercial and enterprise features,
|
||||
supporting advanced business workflows and professional-grade functionality.
|
||||
|
||||
Common use cases:
|
||||
• Enterprise-grade dashboards, multi-tenant deployment, and business intelligence
|
||||
• Organizational hierarchy management and commercial feature licensing
|
||||
• Professional support integration and advanced workflow automation
|
||||
|
||||
Business applications:
|
||||
• ERP integration, CRM development, and executive reporting dashboards
|
||||
• Multi-subsidiary management, professional service delivery, and compliance interfaces
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale deployment management and white-label solution development
|
||||
• Advanced system integration and commercial feature rollout
|
||||
|
||||
Target users: Enterprise administrators, business analysts, and organizations utilizing
|
||||
commercial features and advanced business capabilities.
|
||||
""")
|
||||
public @interface ProprietaryUiDataApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Security API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/security"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(
|
||||
name = "Security",
|
||||
description =
|
||||
"""
|
||||
Document security and protection services for confidential and sensitive content.
|
||||
|
||||
This endpoint group provides essential security operations for organizations handling
|
||||
sensitive documents and materials requiring controlled access.
|
||||
|
||||
Common use cases:
|
||||
• Legal confidentiality, healthcare privacy (HIPAA), and financial regulatory compliance
|
||||
• Government classified handling, corporate IP protection, and educational privacy (FERPA)
|
||||
• Contract security for business transactions
|
||||
|
||||
Business applications:
|
||||
• Document authentication, confidential sharing, and secure archiving
|
||||
• Content watermarking, access control, and privacy protection through redaction
|
||||
|
||||
Industry scenarios:
|
||||
• Legal discovery, medical records exchange, financial audit documentation
|
||||
• Enterprise policy enforcement and data governance
|
||||
|
||||
Target users: Legal professionals, healthcare administrators, compliance officers,
|
||||
government agencies, and enterprises handling sensitive content.
|
||||
""")
|
||||
public @interface SecurityApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Settings API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/settings"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/settings")
|
||||
@Tag(
|
||||
name = "Settings",
|
||||
description =
|
||||
"""
|
||||
User preferences and application customization for personalized workflow optimization.
|
||||
|
||||
This endpoint group provides preference management capabilities for users and
|
||||
organizations to customize their document processing experience.
|
||||
|
||||
Common use cases:
|
||||
• Workflow optimization, accessibility compliance, and corporate branding
|
||||
• Multi-language support, user personalization, and business system integration
|
||||
• Organizational policy compliance
|
||||
|
||||
Business applications:
|
||||
• Corporate branding, productivity optimization, and accessibility compliance
|
||||
• Change management facilitation and training efficiency improvement
|
||||
|
||||
Operational scenarios:
|
||||
• User onboarding, department-specific customization, and system migration
|
||||
• Multi-tenant customization and project-based configuration adjustments
|
||||
|
||||
Target users: End users, department managers, and organizations focused on optimizing
|
||||
user experience and workflow efficiency through personalization.
|
||||
""")
|
||||
public @interface SettingsApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Team Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/team"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/team")
|
||||
@Tag(
|
||||
name = "Team",
|
||||
description =
|
||||
"""
|
||||
Team management and collaboration services for organized document processing workflows.
|
||||
|
||||
This endpoint group enables organizations to structure collaborative document processing
|
||||
activities through team-based organization and resource management.
|
||||
|
||||
Common use cases:
|
||||
• Department-based processing, project collaboration, and cross-functional coordination
|
||||
• Client-specific team isolation, temporary project teams, and training coordination
|
||||
• Compliance team coordination for regulatory processing
|
||||
|
||||
Business applications:
|
||||
• Matrix organization support, client service delivery, and cost center allocation
|
||||
• Scalable collaboration, knowledge management, and team-based quality assurance
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale processing coordination and temporary team formation
|
||||
• M&A integration, remote collaboration, and knowledge transfer management
|
||||
|
||||
Target users: Team leaders, project managers, and organizations requiring structured
|
||||
collaborative environments for document processing activities.
|
||||
""")
|
||||
public @interface TeamApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for UI Data API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/ui-data"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
@Tag(
|
||||
name = "UI Data",
|
||||
description =
|
||||
"""
|
||||
User interface data services for dynamic frontend applications and user experience customization.
|
||||
|
||||
This endpoint group provides data services for frontend applications to render personalized
|
||||
interfaces and deliver optimized experiences based on system configuration.
|
||||
|
||||
Common use cases:
|
||||
• Dynamic UI customization, multi-language support, and feature configuration
|
||||
• Real-time status delivery, corporate branding, and mobile optimization
|
||||
• Progressive web application (PWA) configuration
|
||||
|
||||
Business applications:
|
||||
• Brand customization, user experience optimization, and accessibility compliance
|
||||
• Multi-tenant customization, training support, and performance optimization
|
||||
|
||||
Operational scenarios:
|
||||
• Frontend deployment, UI A/B testing, and system integration
|
||||
• Mobile synchronization and offline capability enhancement
|
||||
|
||||
Target users: Frontend developers, UI/UX designers, and organizations requiring
|
||||
customizable user interfaces and optimized user experiences.
|
||||
""")
|
||||
public @interface UiDataApi {}
|
||||
@@ -1,46 +0,0 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for User Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/user"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
@Tag(
|
||||
name = "User",
|
||||
description =
|
||||
"""
|
||||
User management and authentication services for multi-user and enterprise environments.
|
||||
|
||||
This endpoint group provides user lifecycle management capabilities for organizations
|
||||
deploying Stirling PDF in multi-user scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Employee onboarding/offboarding and corporate access control
|
||||
• Department-based permissions, regulatory compliance, and SSO integration
|
||||
• Multi-tenant deployment and guest user access management
|
||||
|
||||
Business applications:
|
||||
• Enterprise IAM integration, security governance, and cost allocation
|
||||
• Compliance reporting, workflow management, and partner collaboration
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale provisioning, automated HR integration, and emergency access
|
||||
• User migration and self-service profile maintenance
|
||||
|
||||
Target users: IT administrators, HR departments, and organizations requiring
|
||||
structured user management and enterprise identity integration.
|
||||
""")
|
||||
public @interface UserApi {}
|
||||
@@ -1,32 +0,0 @@
|
||||
package stirling.software.common.model.api;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
@Schema(description = "PDF file input - either upload a file or provide a server-side file ID")
|
||||
public class PDFFile {
|
||||
@Schema(description = "The input PDF file", format = "binary")
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
|
||||
private String fileId;
|
||||
|
||||
@AssertTrue(message = "Either fileInput or fileId must be provided")
|
||||
@Schema(hidden = true)
|
||||
private boolean isValid() {
|
||||
return (fileInput != null && (fileId == null || fileId.trim().isEmpty()))
|
||||
|| (fileId != null && !fileId.trim().isEmpty() && fileInput == null);
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
public interface ServerCertificateServiceInterface {
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
boolean hasServerCertificate();
|
||||
|
||||
void initializeServerCertificate();
|
||||
|
||||
KeyStore getServerKeyStore() throws Exception;
|
||||
|
||||
String getServerCertificatePassword();
|
||||
|
||||
X509Certificate getServerCertificate() throws Exception;
|
||||
|
||||
byte[] getServerCertificatePublicKey() throws Exception;
|
||||
|
||||
void uploadServerCertificate(InputStream p12Stream, String password) throws Exception;
|
||||
|
||||
void deleteServerCertificate() throws Exception;
|
||||
|
||||
ServerCertificateInfo getServerCertificateInfo() throws Exception;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class ServerCertificateInfo {
|
||||
private final boolean exists;
|
||||
private final String subject;
|
||||
private final String issuer;
|
||||
private final Date validFrom;
|
||||
private final Date validTo;
|
||||
}
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
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,71 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import org.owasp.html.AttributePolicy;
|
||||
import org.owasp.html.HtmlPolicyBuilder;
|
||||
import org.owasp.html.PolicyFactory;
|
||||
import org.owasp.html.Sanitizers;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.SsrfProtectionService;
|
||||
|
||||
@Component
|
||||
public class CustomHtmlSanitizer {
|
||||
|
||||
private final SsrfProtectionService ssrfProtectionService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired
|
||||
public CustomHtmlSanitizer(
|
||||
SsrfProtectionService ssrfProtectionService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.ssrfProtectionService = ssrfProtectionService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
private final AttributePolicy SSRF_SAFE_URL_POLICY =
|
||||
new AttributePolicy() {
|
||||
@Override
|
||||
public String apply(String elementName, String attributeName, String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmedValue = value.trim();
|
||||
|
||||
// Use the SSRF protection service to validate the URL
|
||||
if (ssrfProtectionService != null
|
||||
&& !ssrfProtectionService.isUrlAllowed(trimmedValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmedValue;
|
||||
}
|
||||
};
|
||||
|
||||
private final PolicyFactory SSRF_SAFE_IMAGES_POLICY =
|
||||
new HtmlPolicyBuilder()
|
||||
.allowElements("img")
|
||||
.allowAttributes("alt", "width", "height", "title")
|
||||
.onElements("img")
|
||||
.allowAttributes("src")
|
||||
.matching(SSRF_SAFE_URL_POLICY)
|
||||
.onElements("img")
|
||||
.toFactory();
|
||||
|
||||
private final PolicyFactory POLICY =
|
||||
Sanitizers.FORMATTING
|
||||
.and(Sanitizers.BLOCKS)
|
||||
.and(Sanitizers.STYLES)
|
||||
.and(Sanitizers.LINKS)
|
||||
.and(Sanitizers.TABLES)
|
||||
.and(SSRF_SAFE_IMAGES_POLICY)
|
||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||
|
||||
public String sanitize(String html) {
|
||||
boolean disableSanitize =
|
||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
||||
return disableSanitize ? html : POLICY.sanitize(html);
|
||||
}
|
||||
}
|
||||
@@ -1,652 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,601 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@UtilityClass
|
||||
public class EmlToPdf {
|
||||
|
||||
public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request)
|
||||
throws IOException {
|
||||
EmlProcessingUtils.validateEmlInput(emlBytes);
|
||||
|
||||
EmlParser.EmailContent emailContent =
|
||||
EmlParser.extractEmailContent(emlBytes, request, null);
|
||||
return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null);
|
||||
}
|
||||
|
||||
public static byte[] convertEmlToPdf(
|
||||
String weasyprintPath,
|
||||
EmlToPdfRequest request,
|
||||
byte[] emlBytes,
|
||||
String fileName,
|
||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||
TempFileManager tempFileManager,
|
||||
CustomHtmlSanitizer customHtmlSanitizer)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
EmlProcessingUtils.validateEmlInput(emlBytes);
|
||||
|
||||
try {
|
||||
EmlParser.EmailContent emailContent =
|
||||
EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer);
|
||||
|
||||
String htmlContent =
|
||||
EmlProcessingUtils.generateEnhancedEmailHtml(
|
||||
emailContent, request, customHtmlSanitizer);
|
||||
|
||||
byte[] pdfBytes =
|
||||
convertHtmlToPdf(
|
||||
weasyprintPath,
|
||||
request,
|
||||
htmlContent,
|
||||
tempFileManager,
|
||||
customHtmlSanitizer);
|
||||
|
||||
if (shouldAttachFiles(emailContent, request)) {
|
||||
pdfBytes =
|
||||
PdfAttachmentHandler.attachFilesToPdf(
|
||||
pdfBytes, emailContent.getAttachments(), pdfDocumentFactory);
|
||||
}
|
||||
|
||||
return pdfBytes;
|
||||
|
||||
} catch (IOException | InterruptedException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IOException("Error converting EML to PDF", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldAttachFiles(
|
||||
EmlParser.EmailContent emailContent, EmlToPdfRequest request) {
|
||||
return emailContent != null
|
||||
&& request != null
|
||||
&& request.isIncludeAttachments()
|
||||
&& !emailContent.getAttachments().isEmpty();
|
||||
}
|
||||
|
||||
private static byte[] convertHtmlToPdf(
|
||||
String weasyprintPath,
|
||||
EmlToPdfRequest request,
|
||||
String htmlContent,
|
||||
TempFileManager tempFileManager,
|
||||
CustomHtmlSanitizer customHtmlSanitizer)
|
||||
throws IOException, InterruptedException {
|
||||
|
||||
var htmlRequest = EmlProcessingUtils.createHtmlRequest(request);
|
||||
|
||||
try {
|
||||
return FileToPdf.convertHtmlToPdf(
|
||||
weasyprintPath,
|
||||
htmlRequest,
|
||||
htmlContent.getBytes(StandardCharsets.UTF_8),
|
||||
"email.html",
|
||||
tempFileManager,
|
||||
customHtmlSanitizer);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
String simplifiedHtml = EmlProcessingUtils.simplifyHtmlContent(htmlContent);
|
||||
return FileToPdf.convertHtmlToPdf(
|
||||
weasyprintPath,
|
||||
htmlRequest,
|
||||
simplifiedHtml.getBytes(StandardCharsets.UTF_8),
|
||||
"email.html",
|
||||
tempFileManager,
|
||||
customHtmlSanitizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,680 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
-248
@@ -1,248 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
ok
|
||||
-188
@@ -1,188 +0,0 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
import io.swagger.v3.oas.models.PathItem;
|
||||
import io.swagger.v3.oas.models.media.Content;
|
||||
import io.swagger.v3.oas.models.media.MediaType;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse;
|
||||
|
||||
/**
|
||||
* Global OpenAPI customizer that adds standard error responses (400, 413, 422, 500) to all API
|
||||
* operations under /api/v1/** paths.
|
||||
*/
|
||||
@Component
|
||||
public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
|
||||
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
if (openApi.getPaths() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
openApi.getPaths()
|
||||
.forEach(
|
||||
(path, pathItem) -> {
|
||||
if (path.startsWith("/api/v1/")) {
|
||||
addErrorResponsesToPathItem(pathItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addErrorResponsesToPathItem(PathItem pathItem) {
|
||||
if (pathItem.getPost() != null) {
|
||||
addStandardErrorResponses(pathItem.getPost());
|
||||
}
|
||||
if (pathItem.getPut() != null) {
|
||||
addStandardErrorResponses(pathItem.getPut());
|
||||
}
|
||||
if (pathItem.getPatch() != null) {
|
||||
addStandardErrorResponses(pathItem.getPatch());
|
||||
}
|
||||
if (pathItem.getDelete() != null) {
|
||||
addStandardErrorResponses(pathItem.getDelete());
|
||||
}
|
||||
if (pathItem.getGet() != null) {
|
||||
addStandardErrorResponses(pathItem.getGet());
|
||||
}
|
||||
}
|
||||
|
||||
private void addStandardErrorResponses(Operation operation) {
|
||||
if (operation.getResponses() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add error responses if they don't already exist
|
||||
if (!operation.getResponses().containsKey("400")) {
|
||||
operation.getResponses().addApiResponse("400", create400Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("413")) {
|
||||
operation.getResponses().addApiResponse("413", create413Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("422")) {
|
||||
operation.getResponses().addApiResponse("422", create422Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("500")) {
|
||||
operation.getResponses().addApiResponse("500", create500Response());
|
||||
}
|
||||
}
|
||||
|
||||
private ApiResponse create400Response() {
|
||||
return new ApiResponse()
|
||||
.description(
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted file")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
400,
|
||||
"Invalid input parameters or corrupted file",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
400,
|
||||
"Invalid input parameters or corrupted file",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create413Response() {
|
||||
return new ApiResponse()
|
||||
.description("Payload too large - File exceeds maximum allowed size")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
413,
|
||||
"File size exceeds maximum allowed limit",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
413,
|
||||
"File size exceeds maximum allowed limit",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create422Response() {
|
||||
return new ApiResponse()
|
||||
.description("Unprocessable entity - File is valid but cannot be processed")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
422,
|
||||
"File is valid but cannot be processed",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
422,
|
||||
"File is valid but cannot be processed",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create500Response() {
|
||||
return new ApiResponse()
|
||||
.description("Internal server error - Unexpected error during processing")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
500,
|
||||
"Unexpected error during processing",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
500,
|
||||
"Unexpected error during processing",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private Schema<?> createErrorSchema(int status, String message, String path) {
|
||||
return new Schema<>()
|
||||
.type("object")
|
||||
.addProperty("status", new Schema<>().type("integer").example(status))
|
||||
.addProperty("error", new Schema<>().type("string").example(getErrorType(status)))
|
||||
.addProperty("message", new Schema<>().type("string").example(message))
|
||||
.addProperty(
|
||||
"timestamp",
|
||||
new Schema<>()
|
||||
.type("string")
|
||||
.format("date-time")
|
||||
.example("2024-01-15T10:30:00Z"))
|
||||
.addProperty("path", new Schema<>().type("string").example(path));
|
||||
}
|
||||
|
||||
private Object createErrorExample(int status, String message, String path) {
|
||||
return java.util.Map.of(
|
||||
"status", status,
|
||||
"error", getErrorType(status),
|
||||
"message", message,
|
||||
"timestamp", "2024-01-15T10:30:00Z",
|
||||
"path", path);
|
||||
}
|
||||
|
||||
private String getErrorType(int status) {
|
||||
return switch (status) {
|
||||
case 400 -> "Bad Request";
|
||||
case 413 -> "Payload Too Large";
|
||||
case 422 -> "Unprocessable Entity";
|
||||
case 500 -> "Internal Server Error";
|
||||
default -> "Error";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.ComposedSchema;
|
||||
import io.swagger.v3.oas.models.media.ObjectSchema;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.media.StringSchema;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class OpenApiConfig {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private static final String DEFAULT_TITLE = "Stirling PDF API";
|
||||
private static final String DEFAULT_DESCRIPTION =
|
||||
"API documentation for all Server-Side processing.\n"
|
||||
+ "Please note some functionality might be UI only and missing from here.";
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
String version = getClass().getPackage().getImplementationVersion();
|
||||
if (version == null) {
|
||||
// default version if all else fails
|
||||
version = "1.0.0";
|
||||
}
|
||||
Info info =
|
||||
new Info()
|
||||
.title(DEFAULT_TITLE)
|
||||
.version(version)
|
||||
.license(
|
||||
new License()
|
||||
.name("MIT")
|
||||
.url(
|
||||
"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE"))
|
||||
.termsOfService("https://www.stirlingpdf.com/terms")
|
||||
.contact(
|
||||
new Contact()
|
||||
.name("Stirling Software")
|
||||
.url("https://www.stirlingpdf.com")
|
||||
.email("contact@stirlingpdf.com"))
|
||||
.description(DEFAULT_DESCRIPTION);
|
||||
|
||||
OpenAPI openAPI = new OpenAPI().info(info).openapi("3.0.3");
|
||||
|
||||
// Add server configuration from environment variable
|
||||
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
|
||||
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
|
||||
Server server = new Server().url(swaggerServerUrl).description("API Server");
|
||||
openAPI.addServersItem(server);
|
||||
}
|
||||
|
||||
// Add ErrorResponse schema to components
|
||||
Schema<?> errorResponseSchema =
|
||||
new Schema<>()
|
||||
.type("object")
|
||||
.addProperty(
|
||||
"timestamp",
|
||||
new Schema<>()
|
||||
.type("string")
|
||||
.format("date-time")
|
||||
.description("Error timestamp"))
|
||||
.addProperty(
|
||||
"status",
|
||||
new Schema<>().type("integer").description("HTTP status code"))
|
||||
.addProperty(
|
||||
"error", new Schema<>().type("string").description("Error type"))
|
||||
.addProperty(
|
||||
"message",
|
||||
new Schema<>().type("string").description("Error message"))
|
||||
.addProperty(
|
||||
"path", new Schema<>().type("string").description("Request path"))
|
||||
.description("Standard error response format");
|
||||
|
||||
Components components = new Components().addSchemas("ErrorResponse", errorResponseSchema);
|
||||
|
||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||
return openAPI.components(components);
|
||||
} else {
|
||||
SecurityScheme apiKeyScheme =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("X-API-KEY");
|
||||
components.addSecuritySchemes("apiKey", apiKeyScheme);
|
||||
return openAPI.components(components)
|
||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
OpenApiCustomizer pdfFileOneOfCustomizer() {
|
||||
return openApi -> {
|
||||
var components = openApi.getComponents();
|
||||
var schemas = components.getSchemas();
|
||||
|
||||
// Define the two shapes
|
||||
var upload =
|
||||
new ObjectSchema()
|
||||
.name("PDFFileUpload")
|
||||
.description("Upload a PDF file")
|
||||
.addProperty("fileInput", new StringSchema().format("binary"))
|
||||
.addRequiredItem("fileInput");
|
||||
|
||||
var ref =
|
||||
new ObjectSchema()
|
||||
.name("PDFFileRef")
|
||||
.description("Reference a server-side file")
|
||||
.addProperty(
|
||||
"fileId",
|
||||
new StringSchema()
|
||||
.example("a1b2c3d4-5678-90ab-cdef-ghijklmnopqr"))
|
||||
.addRequiredItem("fileId");
|
||||
|
||||
schemas.put("PDFFileUpload", upload);
|
||||
schemas.put("PDFFileRef", ref);
|
||||
|
||||
// Create the oneOf schema
|
||||
var pdfFileOneOf =
|
||||
new ComposedSchema()
|
||||
.oneOf(
|
||||
List.of(
|
||||
new Schema<>()
|
||||
.$ref("#/components/schemas/PDFFileUpload"),
|
||||
new Schema<>().$ref("#/components/schemas/PDFFileRef")))
|
||||
.description("Either upload a file or provide a server-side file ID");
|
||||
|
||||
// Replace PDFFile schema
|
||||
schemas.put("PDFFile", pdfFileOneOf);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SpringDocConfig {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi pdfProcessingApi(
|
||||
@Qualifier("pdfFileOneOfCustomizer") OpenApiCustomizer pdfFileOneOfCustomizer) {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("file-processing")
|
||||
.displayName("File Processing")
|
||||
.pathsToMatch("/api/v1/**")
|
||||
.pathsToExclude(
|
||||
"/api/v1/admin/**",
|
||||
"/api/v1/user/**",
|
||||
"/api/v1/settings/**",
|
||||
"/api/v1/team/**",
|
||||
"/api/v1/ui-data/**",
|
||||
"/api/v1/proprietary/ui-data/**",
|
||||
"/api/v1/info/**",
|
||||
"/api/v1/general/job/**",
|
||||
"/api/v1/general/files/**")
|
||||
.addOpenApiCustomizer(pdfFileOneOfCustomizer)
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - Processing API")
|
||||
.description(
|
||||
"API documentation for PDF processing operations including conversion, manipulation, security, and utilities."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi adminApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("management")
|
||||
.displayName("Management")
|
||||
.pathsToMatch(
|
||||
"/api/v1/admin/**",
|
||||
"/api/v1/user/**",
|
||||
"/api/v1/settings/**",
|
||||
"/api/v1/team/**")
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - Admin API")
|
||||
.description(
|
||||
"API documentation for administrative functions, user management, and system configuration."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi systemApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("system")
|
||||
.displayName("System & UI API")
|
||||
.pathsToMatch(
|
||||
"/api/v1/ui-data/**",
|
||||
"/api/v1/proprietary/ui-data/**",
|
||||
"/api/v1/info/**",
|
||||
"/api/v1/general/job/**",
|
||||
"/api/v1/general/files/**")
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - System API")
|
||||
.description(
|
||||
"API documentation for system information, UI data, and utility endpoints."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to CSV conversions. Use for endpoints that convert PDF tables to
|
||||
* CSV format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF tables extracted successfully to CSV format",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "text/csv",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"CSV file containing extracted table data")),
|
||||
@Content(
|
||||
mediaType = "application/zip",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"ZIP archive containing multiple CSV files when multiple tables are extracted"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or no tables found for extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(responseCode = "204", description = "No tables found in the PDF"),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during table extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface CsvConversionResponse {}
|
||||
@@ -1,30 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "Standard error response")
|
||||
public class ErrorResponse {
|
||||
|
||||
@Schema(description = "HTTP status code", example = "400")
|
||||
private int status;
|
||||
|
||||
@Schema(
|
||||
description = "Error message describing what went wrong",
|
||||
example = "Invalid PDF file or corrupted data")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "Timestamp when the error occurred", example = "2024-01-15T10:30:00Z")
|
||||
private String timestamp;
|
||||
|
||||
@Schema(
|
||||
description = "Request path where the error occurred",
|
||||
example = "/api/v1/{endpoint-path}")
|
||||
private String path;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for filter operations that conditionally return the original file. Use
|
||||
* for operations like text filters, page count filters, size filters, etc. Returns the original PDF
|
||||
* if condition is met, otherwise returns no content (204).
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Filter condition met - returns the original PDF file",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "The original PDF file"))),
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "Filter condition not met - no content returned",
|
||||
content = @Content()),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - Invalid filter parameters or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be analyzed for filtering",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during PDF analysis",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface FilterResponse {}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to HTML conversions. Use for endpoints that convert PDF to HTML
|
||||
* format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to HTML format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/html",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "HTML file converted from PDF")))
|
||||
})
|
||||
public @interface HtmlConversionResponse {}
|
||||
@@ -1,34 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for JavaScript extraction from PDFs. Use for endpoints that extract
|
||||
* JavaScript code from PDF documents.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "JavaScript extracted successfully from PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"JavaScript code extracted from PDF")))
|
||||
})
|
||||
public @interface JavaScriptResponse {}
|
||||
@@ -1,47 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for operations that return JSON data or analysis results. Use for
|
||||
* analysis operations, metadata extraction, info operations, etc.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Analysis or data extraction completed successfully",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "object",
|
||||
description =
|
||||
"JSON object containing the requested data or analysis results"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface JsonDataResponse {}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to Markdown conversions. Use for endpoints that convert PDF to
|
||||
* Markdown format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to Markdown format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/markdown",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"Markdown file converted from PDF")))
|
||||
})
|
||||
public @interface MarkdownConversionResponse {}
|
||||
@@ -1,71 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for operations that may return multiple files or a ZIP archive. Use for
|
||||
* operations like PDF to images, split PDF, or multiple file conversions.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description =
|
||||
"Files processed successfully. Returns single file or ZIP archive containing multiple files.",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single PDF file result")),
|
||||
@Content(
|
||||
mediaType = "application/zip",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"ZIP archive containing multiple output files")),
|
||||
@Content(
|
||||
mediaType = "image/png",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single image file (PNG)")),
|
||||
@Content(
|
||||
mediaType = "image/jpeg",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single image file (JPEG)"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface MultiFileResponse {}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to PowerPoint presentation conversions. Use for endpoints that
|
||||
* convert PDF to PPTX format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to PowerPoint presentation",
|
||||
content =
|
||||
@Content(
|
||||
mediaType =
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"Microsoft PowerPoint presentation (PPTX)"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description =
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be converted to PowerPoint format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description =
|
||||
"Internal server error - Unexpected error during PowerPoint conversion",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface PowerPointConversionResponse {}
|
||||
@@ -1,47 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* Standard API response annotation for PDF operations that take PDF input and return PDF output.
|
||||
* Use for single PDF input → single PDF output (SISO) operations like rotate, compress, etc.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF processed successfully",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "The processed PDF file"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface StandardPdfResponse {}
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to plain text conversions. Use for endpoints that extract text
|
||||
* content from PDF.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF text extracted successfully",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
description =
|
||||
"Plain text content extracted from PDF")),
|
||||
@Content(
|
||||
mediaType = "application/rtf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Rich Text Format document"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - Invalid input parameters or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description = "Unprocessable entity - PDF is valid but text extraction failed",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during text extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface TextPlainConversionResponse {}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to Word document conversions. Use for endpoints that convert PDF
|
||||
* to DOCX/DOC formats.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to Word document",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType =
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Microsoft Word document (DOCX)")),
|
||||
@Content(
|
||||
mediaType = "application/msword",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Microsoft Word document (DOC)"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description =
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be converted to Word format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during Word conversion",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface WordConversionResponse {}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to XML conversions. Use for endpoints that convert PDF to XML
|
||||
* format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to XML format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/xml",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "XML file converted from PDF")))
|
||||
})
|
||||
public @interface XmlConversionResponse {}
|
||||
-324
@@ -1,324 +0,0 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class BookletImpositionController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@AutoJobPostMapping(value = "/booklet-imposition", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
summary = "Create a booklet with proper page imposition",
|
||||
description =
|
||||
"This operation combines page reordering for booklet printing with multi-page layout. "
|
||||
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
|
||||
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> createBookletImposition(
|
||||
@ModelAttribute BookletImpositionRequest request) throws IOException {
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
int pagesPerSheet = request.getPagesPerSheet();
|
||||
boolean addBorder = Boolean.TRUE.equals(request.getAddBorder());
|
||||
String spineLocation =
|
||||
request.getSpineLocation() != null ? request.getSpineLocation() : "LEFT";
|
||||
boolean addGutter = Boolean.TRUE.equals(request.getAddGutter());
|
||||
float gutterSize = request.getGutterSize();
|
||||
boolean doubleSided = Boolean.TRUE.equals(request.getDoubleSided());
|
||||
String duplexPass = request.getDuplexPass() != null ? request.getDuplexPass() : "BOTH";
|
||||
boolean flipOnShortEdge = Boolean.TRUE.equals(request.getFlipOnShortEdge());
|
||||
|
||||
// Validate pages per sheet for booklet - only 2-up landscape is proper booklet
|
||||
if (pagesPerSheet != 2) {
|
||||
throw new IllegalArgumentException(
|
||||
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature.");
|
||||
}
|
||||
|
||||
PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
|
||||
// Create proper booklet with signature-based page ordering
|
||||
PDDocument newDocument =
|
||||
createSaddleBooklet(
|
||||
sourceDocument,
|
||||
totalPages,
|
||||
addBorder,
|
||||
spineLocation,
|
||||
addGutter,
|
||||
gutterSize,
|
||||
doubleSided,
|
||||
duplexPass,
|
||||
flipOnShortEdge);
|
||||
|
||||
sourceDocument.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
newDocument.save(baos);
|
||||
newDocument.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||
+ "_booklet.pdf");
|
||||
}
|
||||
|
||||
private static int padToMultipleOf4(int n) {
|
||||
return (n + 3) / 4 * 4;
|
||||
}
|
||||
|
||||
private static class Side {
|
||||
final int left, right;
|
||||
final boolean isBack;
|
||||
|
||||
Side(int left, int right, boolean isBack) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.isBack = isBack;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Side> saddleStitchSides(
|
||||
int totalPagesOriginal,
|
||||
boolean doubleSided,
|
||||
String duplexPass,
|
||||
boolean flipOnShortEdge) {
|
||||
int N = padToMultipleOf4(totalPagesOriginal);
|
||||
List<Side> out = new ArrayList<>();
|
||||
int sheets = N / 4;
|
||||
|
||||
for (int s = 0; s < sheets; s++) {
|
||||
int a = N - 1 - (s * 2); // left, front
|
||||
int b = (s * 2); // right, front
|
||||
int c = (s * 2) + 1; // left, back
|
||||
int d = N - 2 - (s * 2); // right, back
|
||||
|
||||
// clamp to -1 (blank) if >= totalPagesOriginal
|
||||
a = (a < totalPagesOriginal) ? a : -1;
|
||||
b = (b < totalPagesOriginal) ? b : -1;
|
||||
c = (c < totalPagesOriginal) ? c : -1;
|
||||
d = (d < totalPagesOriginal) ? d : -1;
|
||||
|
||||
// Handle duplex pass selection
|
||||
boolean includeFront = "BOTH".equals(duplexPass) || "FIRST".equals(duplexPass);
|
||||
boolean includeBack = "BOTH".equals(duplexPass) || "SECOND".equals(duplexPass);
|
||||
|
||||
if (includeFront) {
|
||||
out.add(new Side(a, b, false)); // front side
|
||||
}
|
||||
|
||||
if (includeBack) {
|
||||
// For short-edge duplex, swap back-side left/right
|
||||
// Note: flipOnShortEdge is ignored in manual duplex mode since users physically
|
||||
// flip the stack
|
||||
if (doubleSided && flipOnShortEdge) {
|
||||
out.add(new Side(d, c, true)); // swapped back side (automatic duplex only)
|
||||
} else {
|
||||
out.add(new Side(c, d, true)); // normal back side
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private PDDocument createSaddleBooklet(
|
||||
PDDocument src,
|
||||
int totalPages,
|
||||
boolean addBorder,
|
||||
String spineLocation,
|
||||
boolean addGutter,
|
||||
float gutterSize,
|
||||
boolean doubleSided,
|
||||
String duplexPass,
|
||||
boolean flipOnShortEdge)
|
||||
throws IOException {
|
||||
|
||||
PDDocument dst = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(src);
|
||||
|
||||
// Derive paper size from source document's first page CropBox
|
||||
PDRectangle srcBox = src.getPage(0).getCropBox();
|
||||
PDRectangle portraitPaper = new PDRectangle(srcBox.getWidth(), srcBox.getHeight());
|
||||
// Force landscape for booklet (Acrobat booklet uses landscape paper to fold to portrait)
|
||||
PDRectangle pageSize = new PDRectangle(portraitPaper.getHeight(), portraitPaper.getWidth());
|
||||
|
||||
// Validate and clamp gutter size
|
||||
if (gutterSize < 0) gutterSize = 0;
|
||||
if (gutterSize >= pageSize.getWidth() / 2f) gutterSize = pageSize.getWidth() / 2f - 1f;
|
||||
|
||||
List<Side> sides = saddleStitchSides(totalPages, doubleSided, duplexPass, flipOnShortEdge);
|
||||
|
||||
for (Side side : sides) {
|
||||
PDPage out = new PDPage(pageSize);
|
||||
dst.addPage(out);
|
||||
|
||||
float cellW = pageSize.getWidth() / 2f;
|
||||
float cellH = pageSize.getHeight();
|
||||
|
||||
// For RIGHT spine (RTL), swap left/right placements
|
||||
boolean rtl = "RIGHT".equalsIgnoreCase(spineLocation);
|
||||
int leftCol = rtl ? 1 : 0;
|
||||
int rightCol = rtl ? 0 : 1;
|
||||
|
||||
// Apply gutter margins with centered gap option
|
||||
float g = addGutter ? gutterSize : 0f;
|
||||
float leftCellX = leftCol * cellW + (g / 2f);
|
||||
float rightCellX = rightCol * cellW - (g / 2f);
|
||||
float leftCellW = cellW - (g / 2f);
|
||||
float rightCellW = cellW - (g / 2f);
|
||||
|
||||
// Create LayerUtility once per page for efficiency
|
||||
LayerUtility layerUtility = new LayerUtility(dst);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(
|
||||
dst, out, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
|
||||
if (addBorder) {
|
||||
cs.setLineWidth(1.5f);
|
||||
cs.setStrokingColor(Color.BLACK);
|
||||
}
|
||||
|
||||
// draw left cell
|
||||
drawCell(
|
||||
src,
|
||||
dst,
|
||||
cs,
|
||||
layerUtility,
|
||||
side.left,
|
||||
leftCellX,
|
||||
0f,
|
||||
leftCellW,
|
||||
cellH,
|
||||
addBorder);
|
||||
// draw right cell
|
||||
drawCell(
|
||||
src,
|
||||
dst,
|
||||
cs,
|
||||
layerUtility,
|
||||
side.right,
|
||||
rightCellX,
|
||||
0f,
|
||||
rightCellW,
|
||||
cellH,
|
||||
addBorder);
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
private void drawCell(
|
||||
PDDocument src,
|
||||
PDDocument dst,
|
||||
PDPageContentStream cs,
|
||||
LayerUtility layerUtility,
|
||||
int pageIndex,
|
||||
float cellX,
|
||||
float cellY,
|
||||
float cellW,
|
||||
float cellH,
|
||||
boolean addBorder)
|
||||
throws IOException {
|
||||
|
||||
if (pageIndex < 0) {
|
||||
// Draw border for blank cell if needed
|
||||
if (addBorder) {
|
||||
cs.addRect(cellX, cellY, cellW, cellH);
|
||||
cs.stroke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PDPage srcPage = src.getPage(pageIndex);
|
||||
PDRectangle r = srcPage.getCropBox(); // Use CropBox instead of MediaBox
|
||||
int rot = (srcPage.getRotation() + 360) % 360;
|
||||
|
||||
// Calculate scale factors, accounting for rotation
|
||||
float sx = cellW / r.getWidth();
|
||||
float sy = cellH / r.getHeight();
|
||||
float s = Math.min(sx, sy);
|
||||
|
||||
// If rotated 90/270 degrees, swap dimensions for fitting
|
||||
if (rot == 90 || rot == 270) {
|
||||
sx = cellW / r.getHeight();
|
||||
sy = cellH / r.getWidth();
|
||||
s = Math.min(sx, sy);
|
||||
}
|
||||
|
||||
float drawnW = (rot == 90 || rot == 270) ? r.getHeight() * s : r.getWidth() * s;
|
||||
float drawnH = (rot == 90 || rot == 270) ? r.getWidth() * s : r.getHeight() * s;
|
||||
|
||||
// Center in cell, accounting for CropBox offset
|
||||
float tx = cellX + (cellW - drawnW) / 2f - r.getLowerLeftX() * s;
|
||||
float ty = cellY + (cellH - drawnH) / 2f - r.getLowerLeftY() * s;
|
||||
|
||||
cs.saveGraphicsState();
|
||||
cs.transform(Matrix.getTranslateInstance(tx, ty));
|
||||
cs.transform(Matrix.getScaleInstance(s, s));
|
||||
|
||||
// Apply rotation if needed (rotate about origin), then translate to keep in cell
|
||||
switch (rot) {
|
||||
case 90:
|
||||
cs.transform(Matrix.getRotateInstance(Math.PI / 2, 0, 0));
|
||||
// After 90° CCW, the content spans x in [-r.getHeight(), 0] and y in [0,
|
||||
// r.getWidth()]
|
||||
cs.transform(Matrix.getTranslateInstance(0, -r.getWidth()));
|
||||
break;
|
||||
case 180:
|
||||
cs.transform(Matrix.getRotateInstance(Math.PI, 0, 0));
|
||||
cs.transform(Matrix.getTranslateInstance(-r.getWidth(), -r.getHeight()));
|
||||
break;
|
||||
case 270:
|
||||
cs.transform(Matrix.getRotateInstance(3 * Math.PI / 2, 0, 0));
|
||||
// After 270° CCW, the content spans x in [0, r.getHeight()] and y in
|
||||
// [-r.getWidth(), 0]
|
||||
cs.transform(Matrix.getTranslateInstance(-r.getHeight(), 0));
|
||||
break;
|
||||
default:
|
||||
// 0°: no-op
|
||||
}
|
||||
|
||||
// Reuse LayerUtility passed from caller
|
||||
PDFormXObject form = layerUtility.importPageAsForm(src, pageIndex);
|
||||
cs.drawForm(form);
|
||||
|
||||
cs.restoreGraphicsState();
|
||||
|
||||
// Draw border on top of form to ensure visibility
|
||||
if (addBorder) {
|
||||
cs.addRect(cellX, cellY, cellW, cellH);
|
||||
cs.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.annotations.api.UiDataApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Slf4j
|
||||
@UiDataApi
|
||||
public class UIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@Operation(summary = "Get home page data")
|
||||
public ResponseEntity<HomeData> getHomeData() {
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
|
||||
HomeData data = new HomeData();
|
||||
data.setShowSurveyFromDocker(showSurveyValue);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/licenses")
|
||||
@Operation(summary = "Get third-party licenses data")
|
||||
public ResponseEntity<LicensesData> getLicensesData() {
|
||||
LicensesData data = new LicensesData();
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
|
||||
try {
|
||||
InputStream is = resource.getInputStream();
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
data.setDependencies(Collections.emptyList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Operation(summary = "Get pipeline configuration data")
|
||||
public ResponseEntity<PipelineData> getPipelineData() {
|
||||
PipelineData data = new PipelineData();
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
|
||||
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load pipeline configs", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
|
||||
data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
|
||||
data.setPipelineConfigs(pipelineConfigs);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Operation(summary = "Get signature form data")
|
||||
public ResponseEntity<SignData> getSignData() {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
List<FontResource> fonts = getFontNames();
|
||||
|
||||
SignData data = new SignData();
|
||||
data.setSignatures(signatures);
|
||||
data.setFonts(fonts);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@Operation(summary = "Get OCR PDF data")
|
||||
public ResponseEntity<OcrData> getOcrPdfData() {
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
|
||||
OcrData data = new OcrData();
|
||||
data.setLanguages(languages);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
private List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
java.io.File[] files = new java.io.File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ java.io.File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
String extension = filename.substring(lastDotIndex + 1);
|
||||
return new FontResource(name, extension);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class HomeData {
|
||||
private boolean showSurveyFromDocker;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LicensesData {
|
||||
private List<Dependency> dependencies;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PipelineData {
|
||||
private List<Map<String, String>> pipelineConfigsWithNames;
|
||||
private List<String> pipelineConfigs;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SignData {
|
||||
private List<SignatureFile> signatures;
|
||||
private List<FontResource> fonts;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OcrData {
|
||||
private List<String> languages;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FontResource {
|
||||
private String name;
|
||||
private String extension;
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
|
||||
private static String getFormatFromExtension(String extension) {
|
||||
switch (extension) {
|
||||
case "ttf":
|
||||
return "truetype";
|
||||
case "woff":
|
||||
return "woff";
|
||||
case "woff2":
|
||||
return "woff2";
|
||||
case "eot":
|
||||
return "embedded-opentype";
|
||||
case "svg":
|
||||
return "svg";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package stirling.software.SPDF.model.api;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode
|
||||
public class HandleDataRequest {
|
||||
|
||||
@Schema(description = "The input files", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile[] fileInput;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Pipeline configuration in JSON format containing name and operations list",
|
||||
type = "string",
|
||||
example =
|
||||
"{\\\"name\\\":\\\"Prepare-pdfs-for-email\\\",\\\"pipeline\\\":[{\\\"operation\\\":\\\"/api/v1/misc/repair\\\",\\\"parameters\\\":{}},{\\\"operation\\\":\\\"/api/v1/security/sanitize-pdf\\\",\\\"parameters\\\":{\\\"removeJavaScript\\\":true,\\\"removeEmbeddedFiles\\\":false}},{\\\"operation\\\":\\\"/api/v1/misc/compress-pdf\\\",\\\"parameters\\\":{\\\"optimizeLevel\\\":2}}]}",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String json;
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package stirling.software.SPDF.model.api.general;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class BookletImpositionRequest extends PDFFile {
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"The number of pages per side for booklet printing (always 2 for proper booklet).",
|
||||
type = "number",
|
||||
defaultValue = "2",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
allowableValues = {"2"})
|
||||
private int pagesPerSheet = 2;
|
||||
|
||||
@Schema(description = "Boolean for if you wish to add border around the pages")
|
||||
private Boolean addBorder = false;
|
||||
|
||||
@Schema(
|
||||
description = "The spine location for the booklet.",
|
||||
type = "string",
|
||||
defaultValue = "LEFT",
|
||||
allowableValues = {"LEFT", "RIGHT"})
|
||||
private String spineLocation = "LEFT";
|
||||
|
||||
@Schema(description = "Add gutter margin (inner margin for binding)")
|
||||
private Boolean addGutter = false;
|
||||
|
||||
@Schema(
|
||||
description = "Gutter margin size in points (used when addGutter is true)",
|
||||
type = "number",
|
||||
defaultValue = "12")
|
||||
private float gutterSize = 12f;
|
||||
|
||||
@Schema(description = "Generate both front and back sides (double-sided printing)")
|
||||
private Boolean doubleSided = true;
|
||||
|
||||
@Schema(
|
||||
description = "For manual duplex: which pass to generate",
|
||||
type = "string",
|
||||
defaultValue = "BOTH",
|
||||
allowableValues = {"BOTH", "FIRST", "SECOND"})
|
||||
private String duplexPass = "BOTH";
|
||||
|
||||
@Schema(description = "Flip back sides for short-edge duplex printing (default is long-edge)")
|
||||
private Boolean flipOnShortEdge = false;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Authentication utility for cookie-based JWT
|
||||
window.JWTManager = {
|
||||
|
||||
// Logout - clear cookies and redirect to login
|
||||
logout: function() {
|
||||
|
||||
// Clear JWT cookie manually (fallback)
|
||||
document.cookie = 'stirling_jwt=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; SameSite=None; Secure';
|
||||
|
||||
// Perform logout request to clear server-side session
|
||||
fetch('/logout', {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
}).then(response => {
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
} else {
|
||||
window.location.href = '/login?logout=true';
|
||||
}
|
||||
}).catch(() => {
|
||||
// If logout fails, let server handle it
|
||||
window.location.href = '/logout';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.fetchWithCsrf = async function(url, options = {}) {
|
||||
function getCsrfToken() {
|
||||
const cookieValue = document.cookie
|
||||
.split('; ')
|
||||
.find(row => row.startsWith('XSRF-TOKEN='))
|
||||
?.split('=')[1];
|
||||
|
||||
if (cookieValue) {
|
||||
return cookieValue;
|
||||
}
|
||||
const csrfElement = document.querySelector('input[name="_csrf"]');
|
||||
return csrfElement ? csrfElement.value : null;
|
||||
}
|
||||
|
||||
// Create a new options object to avoid modifying the passed object
|
||||
const fetchOptions = { ...options };
|
||||
|
||||
// Ensure headers object exists
|
||||
fetchOptions.headers = { ...options.headers };
|
||||
|
||||
// Add CSRF token if available
|
||||
const csrfToken = getCsrfToken();
|
||||
if (csrfToken) {
|
||||
fetchOptions.headers['X-XSRF-TOKEN'] = csrfToken;
|
||||
}
|
||||
|
||||
// Always include credentials to send JWT cookies
|
||||
fetchOptions.credentials = 'include';
|
||||
|
||||
// Make the request
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
// Handle 401 responses (unauthorized)
|
||||
if (response.status === 401) {
|
||||
console.warn('Authentication failed, redirecting to login');
|
||||
window.JWTManager.logout();
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
function compareVersions(version1, version2) {
|
||||
const v1 = version1.split(".");
|
||||
const v2 = version2.split(".");
|
||||
|
||||
for (let i = 0; i < v1.length || i < v2.length; i++) {
|
||||
const n1 = parseInt(v1[i]) || 0;
|
||||
const n2 = parseInt(v2[i]) || 0;
|
||||
|
||||
if (n1 > n2) {
|
||||
return 1;
|
||||
} else if (n1 < n2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getDownloadUrl() {
|
||||
// Only show download for non-Docker installations
|
||||
if (machineType === 'Docker' || machineType === 'Kubernetes') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const baseUrl = 'https://files.stirlingpdf.com/';
|
||||
|
||||
// Determine file based on machine type and security
|
||||
if (machineType === 'Server-jar') {
|
||||
return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar');
|
||||
}
|
||||
|
||||
// Client installations
|
||||
if (machineType.startsWith('Client-')) {
|
||||
const os = machineType.replace('Client-', ''); // win, mac, unix
|
||||
const type = activeSecurity ? '-server-security' : '-server';
|
||||
|
||||
if (os === 'unix') {
|
||||
return baseUrl + os + type + '.jar';
|
||||
} else if (os === 'win') {
|
||||
return baseUrl + os + '-installer.exe';
|
||||
} else if (os === 'mac') {
|
||||
return baseUrl + os + '-installer.dmg';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Function to get translated priority text
|
||||
function getTranslatedPriority(priority) {
|
||||
switch(priority?.toLowerCase()) {
|
||||
case 'urgent': return updatePriorityUrgent;
|
||||
case 'normal': return updatePriorityNormal;
|
||||
case 'minor': return updatePriorityMinor;
|
||||
case 'low': return updatePriorityLow;
|
||||
default: return priority?.toUpperCase() || 'NORMAL';
|
||||
}
|
||||
}
|
||||
|
||||
async function getUpdateSummary() {
|
||||
// Map Java License enum to API types
|
||||
let type = 'normal';
|
||||
if (licenseType === 'PRO') {
|
||||
type = 'pro';
|
||||
} else if (licenseType === 'ENTERPRISE') {
|
||||
type = 'enterprise';
|
||||
}
|
||||
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`;
|
||||
console.log("Fetching update summary from:", url);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log("Response status:", response.status);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} else {
|
||||
console.error("Failed to fetch update summary from Supabase:", response.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch update summary from Supabase:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getFullUpdateInfo() {
|
||||
// Map Java License enum to API types
|
||||
let type = 'normal';
|
||||
if (licenseType === 'PRO') {
|
||||
type = 'pro';
|
||||
} else if (licenseType === 'ENTERPRISE') {
|
||||
type = 'enterprise';
|
||||
}
|
||||
const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`;
|
||||
console.log("Fetching full update info from:", url);
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log("Full update response status:", response.status);
|
||||
if (response.status === 200) {
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} else {
|
||||
console.error("Failed to fetch full update info from Supabase:", response.status);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch full update info from Supabase:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentVersionFromBypass() {
|
||||
const url = "https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/master/build.gradle";
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.status === 200) {
|
||||
const text = await response.text();
|
||||
const versionRegex = /version\s*=\s*['"](\d+\.\d+\.\d+)['"]/;
|
||||
const match = versionRegex.exec(text);
|
||||
if (match) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
throw new Error("Version number not found");
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch latest version from build.gradle:", error);
|
||||
return ""; // Return an empty string if the fetch fails
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForUpdate() {
|
||||
// Initialize the update button as hidden
|
||||
var updateBtn = document.getElementById("update-btn") || null;
|
||||
var updateLink = document.getElementById("update-link") || null;
|
||||
var updateLinkLegacy = document.getElementById("update-link-legacy") || null;
|
||||
if (updateBtn !== null) {
|
||||
updateBtn.style.display = "none";
|
||||
updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary");
|
||||
}
|
||||
if (updateLink !== null) {
|
||||
updateLink.style.display = "none";
|
||||
}
|
||||
if (updateLinkLegacy !== null) {
|
||||
console.log("hidden!");
|
||||
if (!updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.add("visually-hidden");
|
||||
}
|
||||
}
|
||||
|
||||
const updateSummary = await getUpdateSummary();
|
||||
if (!updateSummary) {
|
||||
console.log("No update summary available");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("updateSummary=", updateSummary);
|
||||
console.log("currentVersion=" + currentVersion);
|
||||
console.log("latestVersion=" + updateSummary.latest_version);
|
||||
|
||||
if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) {
|
||||
const priority = updateSummary.max_priority || 'normal';
|
||||
|
||||
if (updateBtn != null) {
|
||||
// Style button based on priority
|
||||
if (priority === 'urgent') {
|
||||
updateBtn.classList.add("btn-danger");
|
||||
updateBtn.innerHTML = urgentUpdateAvailable;
|
||||
} else if (priority === 'normal') {
|
||||
updateBtn.classList.add("btn-warning");
|
||||
updateBtn.innerHTML = updateAvailableText;
|
||||
} else {
|
||||
updateBtn.classList.add("btn-outline-primary");
|
||||
updateBtn.innerHTML = updateAvailableText;
|
||||
}
|
||||
|
||||
// Store summary for initial display
|
||||
updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary));
|
||||
updateBtn.style.display = "block";
|
||||
|
||||
// Add click handler for update details modal
|
||||
updateBtn.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
showUpdateModal();
|
||||
};
|
||||
}
|
||||
if (updateLink !== null) {
|
||||
document.getElementById("update-link").style.display = "flex";
|
||||
}
|
||||
if (updateLinkLegacy !== null) {
|
||||
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + updateSummary.latest_version + '</b>');
|
||||
if (updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.remove("visually-hidden");
|
||||
}
|
||||
}
|
||||
console.log("visible");
|
||||
} else {
|
||||
if (updateLinkLegacy !== null) {
|
||||
if (!updateLinkLegacy.classList.contains("visually-hidden")) {
|
||||
updateLinkLegacy.classList.add("visually-hidden");
|
||||
}
|
||||
}
|
||||
console.log("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
async function showUpdateModal() {
|
||||
// Close settings modal if open
|
||||
const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal'));
|
||||
if (settingsModal) {
|
||||
settingsModal.hide();
|
||||
}
|
||||
|
||||
// Get summary data from button
|
||||
const updateBtn = document.getElementById("update-btn");
|
||||
const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary'));
|
||||
|
||||
// Utility function to escape HTML special characters
|
||||
function escapeHtml(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
|
||||
// Create initial modal with loading state
|
||||
const initialModalHtml = `
|
||||
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog" aria-labelledby="updateModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document" style="max-height: 80vh;">
|
||||
<div class="modal-content" style="max-height: 80vh;">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="updateModalLabel">${updateModalTitle}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="updateModalBody" style="max-height: 60vh; overflow-y: auto;">
|
||||
<div class="update-summary mb-4">
|
||||
<div class="row mb-3">
|
||||
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
|
||||
<small class="text-muted">${updateCurrent}</small><br>
|
||||
<strong>${escapeHtml(currentVersion)}</strong>
|
||||
</div>
|
||||
<div class="${summaryData.latest_stable_version ? 'col-4' : 'col-6'} text-center">
|
||||
<small class="text-muted">${updateLatest}</small><br>
|
||||
<strong class="text-primary">${escapeHtml(summaryData.latest_version)}</strong>
|
||||
</div>
|
||||
${summaryData.latest_stable_version ? `
|
||||
<div class="col-4 text-center">
|
||||
<small class="text-muted">${updateLatestStable}</small><br>
|
||||
<strong class="text-success">${escapeHtml(summaryData.latest_stable_version)}</strong>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="alert ${summaryData.max_priority === 'urgent' ? 'alert-danger' : 'alert-warning'}" role="alert">
|
||||
<strong>${updatePriority}:</strong> ${getTranslatedPriority(summaryData.max_priority)}
|
||||
${summaryData.recommended_action ? `<br><strong>${updateRecommendedAction}:</strong> ${escapeHtml(summaryData.recommended_action)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${summaryData.any_breaking ? `
|
||||
<div class="alert alert-warning" role="alert">
|
||||
<h6><strong>${updateBreakingChangesDetected}</strong></h6>
|
||||
<p>${updateBreakingChangesMessage}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${summaryData.migration_guides && summaryData.migration_guides.length > 0 ? `
|
||||
<div class="migration-guides mb-4">
|
||||
<h6>${updateMigrationGuides}</h6>
|
||||
<ul class="list-group">
|
||||
${summaryData.migration_guides.map(guide => `
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${updateVersion} ${escapeHtml(guide.version)}:</strong> ${escapeHtml(guide.notes)}
|
||||
</div>
|
||||
<a href="${escapeHtml(guide.url)}" target="_blank" class="btn btn-sm btn-outline-primary">${updateViewGuide}</a>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status" id="loadingSpinner">
|
||||
<span class="visually-hidden">${updateLoadingDetailedInfo}</span>
|
||||
</div>
|
||||
<p class="mt-2">${updateLoadingDetailedInfo}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">${updateClose}</button>
|
||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" class="btn btn-outline-primary">${updateViewAllReleases}</a>
|
||||
${getDownloadUrl() ? `<a href="${escapeHtml(getDownloadUrl())}" class="btn btn-success" target="_blank">${updateDownloadLatest}</a>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove existing modal if present
|
||||
const existingModal = document.getElementById('updateModal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
// Add modal to body
|
||||
document.body.insertAdjacentHTML('beforeend', initialModalHtml);
|
||||
|
||||
// Show modal
|
||||
const modal = new bootstrap.Modal(document.getElementById('updateModal'));
|
||||
modal.show();
|
||||
|
||||
// Fetch full update info
|
||||
const fullUpdateInfo = await getFullUpdateInfo();
|
||||
|
||||
// Update modal with full information
|
||||
const modalBody = document.getElementById('updateModalBody');
|
||||
if (fullUpdateInfo && fullUpdateInfo.new_versions) {
|
||||
const storedMode = localStorage.getItem("dark-mode");
|
||||
const isDarkMode = storedMode === "on" ||
|
||||
(storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
const darkClasses = isDarkMode ? {
|
||||
accordionItem: 'bg-dark border-secondary text-light',
|
||||
accordionButton: 'bg-dark text-light border-secondary',
|
||||
accordionBody: 'bg-dark text-light'
|
||||
} : {
|
||||
accordionItem: '',
|
||||
accordionButton: '',
|
||||
accordionBody: ''
|
||||
};
|
||||
|
||||
const detailedVersionsHtml = `
|
||||
<div class="detailed-versions mt-4">
|
||||
<h6>${updateAvailableUpdates}</h6>
|
||||
<div class="accordion" id="versionsAccordion">
|
||||
${fullUpdateInfo.new_versions.map((version, index) => `
|
||||
<div class="accordion-item" style="border-color: var(--md-sys-color-outline);">
|
||||
<h2 class="accordion-header" id="heading${index}">
|
||||
<button class="accordion-button ${index === 0 ? '' : 'collapsed'}" style="color: var(--md-sys-color-on-surface); background-color:
|
||||
var(--md-sys-color-surface);" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse${index}" aria-expanded="${index === 0 ? 'true' : 'false'}" aria-controls="collapse${index}">
|
||||
<div class="d-flex justify-content-between w-100 me-3">
|
||||
<span><strong>${updateVersion} ${version.version}</strong></span>
|
||||
<span class="badge ${version.priority === 'urgent' ? 'bg-danger' : version.priority === 'normal' ? 'bg-warning' : 'bg-secondary'}">${getTranslatedPriority(version.priority)}</span>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapse${index}" class="accordion-collapse collapse ${index === 0 ? 'show' : ''}"
|
||||
aria-labelledby="heading${index}" data-bs-parent="#versionsAccordion">
|
||||
<div class="accordion-body" style="color: var(--md-sys-color-on-surface); background-color:
|
||||
var(--md-sys-color-surface-bright);">
|
||||
<h6>${version.announcement.title}</h6>
|
||||
<p>${version.announcement.message}</p>
|
||||
${version.compatibility.breaking_changes ? `
|
||||
<div class="alert alert-warning alert-sm" role="alert">
|
||||
<small><strong>⚠️ ${updateBreakingChanges}</strong> ${version.compatibility.breaking_description || updateBreakingChangesDefault}</small>
|
||||
${version.compatibility.migration_guide_url ? `<br><a href="${version.compatibility.migration_guide_url}" target="_blank" class="btn btn-sm btn-outline-warning mt-1">${updateMigrationGuide}</a>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Remove loading spinner and add detailed info
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.parentElement.remove();
|
||||
}
|
||||
modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml);
|
||||
|
||||
} else {
|
||||
// Remove loading spinner if failed to load
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
if (spinner) {
|
||||
spinner.parentElement.innerHTML = `<p class="text-muted">${updateUnableToLoadDetails}</p>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", (event) => {
|
||||
checkForUpdate();
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
// JWT Authentication Management Script
|
||||
// This script handles cookie-based JWT authentication and page access control
|
||||
|
||||
(function() {
|
||||
// Clean up JWT token from URL parameters after OAuth/Login flows
|
||||
function cleanupTokenFromUrl() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const hasToken = urlParams.get('jwt') || urlParams.get('token');
|
||||
if (hasToken) {
|
||||
// Clean up URL by removing token parameter
|
||||
// Token should now be set as cookie by server
|
||||
urlParams.delete('jwt');
|
||||
urlParams.delete('token');
|
||||
const newUrl = window.location.pathname + (urlParams.toString() ? '?' + urlParams.toString() : '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize JWT handling when page loads
|
||||
function initializeJWT() {
|
||||
// Clean up any JWT tokens from URL (OAuth flow)
|
||||
cleanupTokenFromUrl();
|
||||
|
||||
// Authentication is handled server-side
|
||||
// If user is not authenticated, server will redirect to login
|
||||
console.log('JWT initialization complete - authentication handled server-side');
|
||||
}
|
||||
|
||||
// No form enhancement needed for cookie-based JWT
|
||||
// Cookies are automatically sent with form submissions
|
||||
function enhanceFormSubmissions() {
|
||||
// Cookie-based JWT is automatically included in form submissions
|
||||
// No additional processing needed
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeJWT();
|
||||
});
|
||||
} else {
|
||||
initializeJWT();
|
||||
}
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user