Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca8295b02e |
9
.github/workflows/PR-Demo-Comment.yml
vendored
9
.github/workflows/PR-Demo-Comment.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -103,10 +103,9 @@ jobs:
|
|||||||
run: ./gradlew clean build
|
run: ./gradlew clean build
|
||||||
env:
|
env:
|
||||||
DOCKER_ENABLE_SECURITY: false
|
DOCKER_ENABLE_SECURITY: false
|
||||||
STIRLING_PDF_DESKTOP_UI: false
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
@@ -121,7 +120,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push PR-specific image
|
- name: Build and push PR-specific image
|
||||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
2
.github/workflows/PR-Demo-cleanup.yml
vendored
2
.github/workflows/PR-Demo-cleanup.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/auto-labeler.yml
vendored
2
.github/workflows/auto-labeler.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -37,6 +37,12 @@ jobs:
|
|||||||
java-version: ${{ matrix.jdk-version }}
|
java-version: ${{ matrix.jdk-version }}
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- name: PR | Generate verification metadata with signatures and checksums for dependabot[bot]
|
||||||
|
if: github.event.pull_request.user.login == 'dependabot[bot]'
|
||||||
|
run: |
|
||||||
|
./gradlew clean dependencies buildEnvironment spotlessApply --write-verification-metadata sha256 --refresh-dependencies help
|
||||||
|
./gradlew clean dependencies buildEnvironment spotlessApply --write-verification-metadata sha256,pgp --refresh-keys --export-keys --refresh-dependencies help
|
||||||
|
|
||||||
- name: Build with Gradle and no spring security
|
- name: Build with Gradle and no spring security
|
||||||
run: ./gradlew clean build
|
run: ./gradlew clean build
|
||||||
env:
|
env:
|
||||||
@@ -49,7 +55,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Test Reports
|
- name: Upload Test Reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: test-reports-jdk-${{ matrix.jdk-version }}
|
name: test-reports-jdk-${{ matrix.jdk-version }}
|
||||||
path: |
|
path: |
|
||||||
@@ -62,7 +68,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -80,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
- name: FAILED - check the licenses for compatibility
|
- name: FAILED - check the licenses for compatibility
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: dependencies-without-allowed-license.json
|
name: dependencies-without-allowed-license.json
|
||||||
path: |
|
path: |
|
||||||
@@ -106,7 +112,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -120,7 +126,7 @@ jobs:
|
|||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Install Docker Compose
|
- name: Install Docker Compose
|
||||||
run: |
|
run: |
|
||||||
@@ -141,4 +147,4 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
chmod +x ./testing/test_webpages.sh
|
chmod +x ./testing/test_webpages.sh
|
||||||
chmod +x ./testing/test.sh
|
chmod +x ./testing/test.sh
|
||||||
./testing/test.sh
|
./testing/test.sh "${{ github.event.pull_request.user.login == 'dependabot[bot]' }}"
|
||||||
|
|||||||
2
.github/workflows/check_properties.yml
vendored
2
.github/workflows/check_properties.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
10
.github/workflows/licenses-update.yml
vendored
10
.github/workflows/licenses-update.yml
vendored
@@ -18,13 +18,13 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -38,14 +38,14 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
|
|
||||||
- name: check the licenses for compatibility
|
- name: check the licenses for compatibility
|
||||||
run: ./gradlew clean checkLicense
|
run: ./gradlew clean checkLicense
|
||||||
|
|
||||||
- name: FAILED - check the licenses for compatibility
|
- name: FAILED - check the licenses for compatibility
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: dependencies-without-allowed-license.json
|
name: dependencies-without-allowed-license.json
|
||||||
path: |
|
path: |
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: "Update 3rd Party Licenses"
|
commit-message: "Update 3rd Party Licenses"
|
||||||
|
|||||||
2
.github/workflows/manage-label.yml
vendored
2
.github/workflows/manage-label.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
26
.github/workflows/multiOSReleases.yml
vendored
26
.github/workflows/multiOSReleases.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.12
|
gradle-version: 8.12
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -101,7 +101,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ jobs:
|
|||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.12
|
gradle-version: 8.12
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
run: ls -R ./binaries
|
run: ls -R ./binaries
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -210,7 +210,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@@ -255,7 +255,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -271,7 +271,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/pre_commit.yml
vendored
6
.github/workflows/pre_commit.yml
vendored
@@ -16,13 +16,13 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: ":file_folder: pre-commit"
|
commit-message: ":file_folder: pre-commit"
|
||||||
|
|||||||
17
.github/workflows/push-docker.yml
vendored
17
.github/workflows/push-docker.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.12
|
gradle-version: 8.12
|
||||||
|
|
||||||
@@ -38,17 +38,16 @@ jobs:
|
|||||||
run: ./gradlew clean build
|
run: ./gradlew clean build
|
||||||
env:
|
env:
|
||||||
DOCKER_ENABLE_SECURITY: false
|
DOCKER_ENABLE_SECURITY: false
|
||||||
STIRLING_PDF_DESKTOP_UI: false
|
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
@@ -68,7 +67,7 @@ jobs:
|
|||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0
|
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Convert repository owner to lowercase
|
||||||
id: repoowner
|
id: repoowner
|
||||||
@@ -90,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
id: build-push-regular
|
id: build-push-regular
|
||||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -135,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
id: build-push-lite
|
id: build-push-lite
|
||||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -166,7 +165,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
id: build-push-fat
|
id: build-push-fat
|
||||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
|||||||
14
.github/workflows/releaseArtifacts.yml
vendored
14
.github/workflows/releaseArtifacts.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.12
|
gradle-version: 8.12
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
ls -R ./build/launch4j
|
ls -R ./build/launch4j
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: binaries${{ matrix.file_suffix }}
|
name: binaries${{ matrix.file_suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
run: cosign generate-key-pair
|
run: cosign generate-key-pair
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: signed${{ matrix.file_suffix }}
|
name: signed${{ matrix.file_suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -161,7 +161,7 @@ jobs:
|
|||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/scorecards.yml
vendored
8
.github/workflows/scorecards.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: "Run analysis"
|
- name: "Run analysis"
|
||||||
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
@@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
70
.github/workflows/sonarqube.yml
vendored
70
.github/workflows/sonarqube.yml
vendored
@@ -1,63 +1,37 @@
|
|||||||
name: Run Sonarqube
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
pull_request_target:
|
pull_request:
|
||||||
branches:
|
branches: [ "main" ]
|
||||||
- main
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
actions: read
|
name: Run Sonarqube
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sonarqube:
|
sonarqube:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Analyze with SonarCloud
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
|
||||||
with:
|
|
||||||
egress-policy: audit
|
|
||||||
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
# You can pin the exact commit or the version.
|
||||||
with:
|
# uses: SonarSource/sonarcloud-github-action@v2.2.0
|
||||||
fetch-depth: 0
|
uses: SonarSource/sonarcloud-github-action@4006f663ecaf1f8093e8e4abb9227f6041f52216 #v2.2.0
|
||||||
|
|
||||||
- name: Setup Gradle
|
|
||||||
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
|
||||||
|
|
||||||
- name: Build and analyze with Gradle
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret)
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
DOCKER_ENABLE_SECURITY: true
|
|
||||||
STIRLING_PDF_DESKTOP_UI: true
|
|
||||||
run: |
|
|
||||||
./gradlew clean build sonar \
|
|
||||||
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF \
|
|
||||||
-Dsonar.organization=stirling-tools \
|
|
||||||
-Dsonar.host.url=https://sonarcloud.io \
|
|
||||||
-Dsonar.login=${SONAR_TOKEN} \
|
|
||||||
-Dsonar.log.level=DEBUG \
|
|
||||||
--info
|
|
||||||
|
|
||||||
- name: Upload Problems Report on Failure
|
|
||||||
if: failure()
|
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
|
||||||
with:
|
with:
|
||||||
name: gradle-problems-report
|
# Additional arguments for the SonarScanner CLI
|
||||||
path: build/reports/problems/problems-report.html
|
args:
|
||||||
retention-days: 7
|
# Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu)
|
||||||
|
# mandatory
|
||||||
- name: Upload Sonar Logs on Failure
|
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF
|
||||||
if: failure()
|
-Dsonar.organization=stirling-tools
|
||||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
# Comma-separated paths to directories containing main source files.
|
||||||
with:
|
#-Dsonar.sources= # optional, default is project base directory
|
||||||
name: sonar-logs
|
# Comma-separated paths to directories containing test source files.
|
||||||
path: |
|
#-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/
|
||||||
.scannerwork/report-task.txt
|
# Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing.
|
||||||
build/sonar/
|
#-Dsonar.verbose= # optional, default is false
|
||||||
retention-days: 7
|
# When you need the analysis to take place in a directory other than the one from which it was launched, default is .
|
||||||
|
projectBaseDir: .
|
||||||
|
|||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/swagger.yml
vendored
4
.github/workflows/swagger.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|||||||
39
.github/workflows/sync_files.yml
vendored
39
.github/workflows/sync_files.yml
vendored
@@ -8,6 +8,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "build.gradle"
|
- "build.gradle"
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- "gradle/verification-keyring.keys"
|
||||||
|
- "gradle/verification-metadata.xml"
|
||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
- "src/main/resources/static/3rdPartyLicenses.json"
|
- "src/main/resources/static/3rdPartyLicenses.json"
|
||||||
- "scripts/ignore_translation.toml"
|
- "scripts/ignore_translation.toml"
|
||||||
@@ -24,13 +26,13 @@ jobs:
|
|||||||
committer: ${{ steps.committer.outputs.committer }}
|
committer: ${{ steps.committer.outputs.committer }}
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -57,13 +59,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
uses: actions/create-github-app-token@136412a57a7081aa63c935a2cc2918f76c34f514 # v1.11.2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ vars.GH_APP_ID }}
|
app-id: ${{ vars.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -102,8 +104,24 @@ jobs:
|
|||||||
git add README.md
|
git add README.md
|
||||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "no changes"
|
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "no changes"
|
||||||
|
|
||||||
|
- name: Generate verification metadata with signatures and checksums
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
if [ -f ./gradle/verification-metadata.xml ]; then
|
||||||
|
rm ./gradle/verification-metadata.xml
|
||||||
|
fi
|
||||||
|
./gradlew clean dependencies buildEnvironment spotlessApply --write-verification-metadata sha256 help
|
||||||
|
./gradlew clean dependencies buildEnvironment spotlessApply --write-verification-metadata sha256,pgp --refresh-keys --export-keys --refresh-dependencies help
|
||||||
|
./gradlew clean build
|
||||||
|
|
||||||
|
- name: Run git add
|
||||||
|
run: |
|
||||||
|
git add gradle/verification-keyring.keys
|
||||||
|
git add gradle/verification-metadata.xml
|
||||||
|
git diff --staged --quiet || git commit -m ":memo: Generate verification metadata with signatures and checksums" || echo "no changes"
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
@@ -111,11 +129,11 @@ jobs:
|
|||||||
author: ${{ needs.read_bot_entries.outputs.committer }}
|
author: ${{ needs.read_bot_entries.outputs.committer }}
|
||||||
signoff: true
|
signoff: true
|
||||||
branch: sync_readme
|
branch: sync_readme
|
||||||
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
|
title: ":globe_with_meridians: Sync Translations + Update README Progress Table + Update Verification Metadata"
|
||||||
body: |
|
body: |
|
||||||
### Description of Changes
|
### Description of Changes
|
||||||
|
|
||||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
This Pull Request was automatically generated to synchronize updates to translation files, verification metadata, and documentation. Below are the details of the changes made:
|
||||||
|
|
||||||
#### **1. Synchronization of Translation Files**
|
#### **1. Synchronization of Translation Files**
|
||||||
- Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`.
|
- Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`.
|
||||||
@@ -127,9 +145,14 @@ jobs:
|
|||||||
- Added a summary of the current translation status for all supported languages.
|
- Added a summary of the current translation status for all supported languages.
|
||||||
- Included up-to-date statistics on translation coverage.
|
- Included up-to-date statistics on translation coverage.
|
||||||
|
|
||||||
|
#### **3. Verification Metadata Updates**
|
||||||
|
- Generated or refreshed the `verification-keyring.keys` and `verification-metadata.xml` files.
|
||||||
|
- Included the latest dependency signatures and checksums to enhance the build's integrity.
|
||||||
|
|
||||||
#### **Why these changes are necessary**
|
#### **Why these changes are necessary**
|
||||||
- Keeps translation files aligned with the latest reference updates.
|
- Keeps translation files aligned with the latest reference updates.
|
||||||
- Ensures the documentation reflects the current translation progress.
|
- Ensures the documentation reflects the current translation progress.
|
||||||
|
- Strengthens dependency verification for a more secure build process.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -143,3 +166,5 @@ jobs:
|
|||||||
add-paths: |
|
add-paths: |
|
||||||
README.md
|
README.md
|
||||||
src/main/resources/messages_*.properties
|
src/main/resources/messages_*.properties
|
||||||
|
gradle/verification-keyring.keys
|
||||||
|
gradle/verification-metadata.xml
|
||||||
|
|||||||
10
.github/workflows/testdriver.yml
vendored
10
.github/workflows/testdriver.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
DOCKER_ENABLE_SECURITY: false
|
DOCKER_ENABLE_SECURITY: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push test image
|
- name: Build and push test image
|
||||||
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
|
uses: step-security/harden-runner@cb605e52c26070c328afc4562f0b4ada7618a84e # v2.10.4
|
||||||
with:
|
with:
|
||||||
egress-policy: audit
|
egress-policy: audit
|
||||||
|
|
||||||
|
|||||||
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
@@ -9,6 +9,7 @@
|
|||||||
// "ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
// "ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
|
||||||
"Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
"Oracle.oracle-java", // Oracle Java extension with additional features for Java development
|
||||||
"redhat.java", // Java support by Red Hat with IntelliSense, debugging, and code navigation
|
"redhat.java", // Java support by Red Hat with IntelliSense, debugging, and code navigation
|
||||||
|
"shengchen.vscode-checkstyle", // Checkstyle integration for Java code quality checks
|
||||||
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
|
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
|
||||||
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
|
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
|
||||||
"vmware.vscode-spring-boot", // Spring Boot tools by VMware for enhanced Spring development
|
"vmware.vscode-spring-boot", // Spring Boot tools by VMware for enhanced Spring development
|
||||||
|
|||||||
121
.vscode/settings.json
vendored
121
.vscode/settings.json
vendored
@@ -2,147 +2,54 @@
|
|||||||
"java.compile.nullAnalysis.mode": "automatic",
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
"files.eol": "auto",
|
"files.eol": "auto",
|
||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
"black-formatter.args": [
|
"black-formatter.args": ["--line-length", "127"],
|
||||||
"--line-length",
|
"flake8.args": ["--max-line-length", "127"],
|
||||||
"127"
|
"pylint.args": ["max-line-length", "127"],
|
||||||
],
|
|
||||||
"flake8.args": [
|
|
||||||
"--max-line-length",
|
|
||||||
"127"
|
|
||||||
],
|
|
||||||
"[java]": {
|
"[java]": {
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127]
|
||||||
127
|
|
||||||
],
|
|
||||||
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
|
||||||
},
|
},
|
||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127]
|
||||||
127
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"[gradle-build]": {
|
"[gradle-build]": {
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127]
|
||||||
127
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"[gradle]": {
|
"[gradle]": {
|
||||||
"editor.tabSize": 4,
|
"editor.tabSize": 4,
|
||||||
"editor.detectIndentation": false,
|
"editor.detectIndentation": false,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127]
|
||||||
127
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"[html]": {
|
"[html]": {
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127],
|
||||||
127
|
|
||||||
],
|
|
||||||
"files.trimFinalNewlines": false,
|
"files.trimFinalNewlines": false,
|
||||||
"files.insertFinalNewline": false
|
"files.insertFinalNewline": false
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"editor.rulers": [
|
"editor.rulers": [127]
|
||||||
127
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"[yaml]": {
|
"[yaml]": {
|
||||||
"files.trimFinalNewlines": false,
|
"files.trimFinalNewlines": false,
|
||||||
"files.insertFinalNewline": false
|
"files.insertFinalNewline": false
|
||||||
},
|
},
|
||||||
|
"diffEditor.maxComputationTime": 0,
|
||||||
|
"editor.wordSegmenterLocales": null,
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
"files.insertFinalNewline": true,
|
"files.insertFinalNewline": true,
|
||||||
"files.trimFinalNewlines": true,
|
"files.trimFinalNewlines": true,
|
||||||
"files.trimTrailingWhitespace": true,
|
"files.trimTrailingWhitespace": true,
|
||||||
"files.autoSave": "onFocusChange",
|
|
||||||
"files.autoSaveWhenNoErrors": true,
|
|
||||||
"diffEditor.maxComputationTime": 0,
|
|
||||||
"editor.wordSegmenterLocales": "",
|
|
||||||
"editor.guides.bracketPairs": "active",
|
|
||||||
"editor.guides.bracketPairsHorizontal": "active",
|
|
||||||
"editor.indentSize": "tabSize",
|
"editor.indentSize": "tabSize",
|
||||||
"editor.stickyScroll.enabled": false,
|
"editor.stickyScroll.enabled": false,
|
||||||
"editor.minimap.enabled": false,
|
"editor.minimap.enabled": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.insertSpaces": true,
|
|
||||||
"java.format.enabled": true,
|
|
||||||
"java.format.settings.profile": "GoogleStyle",
|
|
||||||
"java.format.settings.google.version": "1.25.2",
|
|
||||||
"java.format.settings.google.mode": "jar-file",
|
"java.format.settings.google.mode": "jar-file",
|
||||||
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
|
"java.format.settings.google.extra": "--aosp --skip-sorting-imports"
|
||||||
// (DE) Aktiviert Kommentare im Java-Format.
|
|
||||||
// (EN) Enables comments in Java formatting.
|
|
||||||
// "java.format.comments.enabled": true,
|
|
||||||
// (DE) Generiert automatisch Kommentare im Code.
|
|
||||||
// (EN) Automatically generates comments in code.
|
|
||||||
// "java.codeGeneration.generateComments": true,
|
|
||||||
// https://github.com/redhat-developer/vscode-java/blob/master/document/_java.learnMoreAboutCleanUps.md#java-clean-ups
|
|
||||||
"java.saveActions.cleanup": true,
|
|
||||||
"java.cleanup.actions": [
|
|
||||||
"invertEquals", // Inverts calls to Object.equals(Object) and String.equalsIgnoreCase(String) to avoid useless null pointer exception.
|
|
||||||
"instanceofPatternMatch" // Replaces instanceof checks with pattern matching.
|
|
||||||
],
|
|
||||||
// (DE) Aktiviert die Code-Vervollständigung für Java.
|
|
||||||
// (EN) Enables code completion for Java.
|
|
||||||
"java.completion.engine": "dom",
|
|
||||||
"java.completion.enabled": true,
|
|
||||||
"java.completion.importOrder": [
|
|
||||||
"java",
|
|
||||||
"javax",
|
|
||||||
"org",
|
|
||||||
"com",
|
|
||||||
"net",
|
|
||||||
"io",
|
|
||||||
"jakarta",
|
|
||||||
"lombok",
|
|
||||||
"me",
|
|
||||||
"stirling",
|
|
||||||
],
|
|
||||||
"java.project.resourceFilters": [
|
|
||||||
".devcontainer/",
|
|
||||||
".git/",
|
|
||||||
".github/",
|
|
||||||
".gradle/",
|
|
||||||
".venv/",
|
|
||||||
".venv*/",
|
|
||||||
".vscode/",
|
|
||||||
"bin/",
|
|
||||||
"build/",
|
|
||||||
"configs/",
|
|
||||||
"customFiles/",
|
|
||||||
"docs/",
|
|
||||||
"exampleYmlFiles",
|
|
||||||
"gradle/",
|
|
||||||
"images/",
|
|
||||||
"logs/",
|
|
||||||
"pipeline/",
|
|
||||||
"scripts/",
|
|
||||||
"testings/",
|
|
||||||
".git-blame-ignore-revs",
|
|
||||||
".gitattributes",
|
|
||||||
".gitignore",
|
|
||||||
".pre-commit-config.yaml",
|
|
||||||
],
|
|
||||||
// Enables signature help in Java.
|
|
||||||
"java.signatureHelp.enabled": true,
|
|
||||||
// Enables detailed signature help descriptions.
|
|
||||||
"java.signatureHelp.description.enabled": true,
|
|
||||||
// Downloads sources for Maven dependencies.
|
|
||||||
"java.maven.downloadSources": true,
|
|
||||||
// Enables Gradle project import.
|
|
||||||
"java.import.gradle.enabled": true,
|
|
||||||
// Downloads sources for Eclipse projects.
|
|
||||||
"java.eclipse.downloadSources": true,
|
|
||||||
// Enables import of the Gradle wrapper.
|
|
||||||
"java.import.gradle.wrapper.enabled": true,
|
|
||||||
"spring.initializr.defaultLanguage": "Java",
|
|
||||||
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
|
||||||
"spring.initializr.defaultArtifactId": "SPDF",
|
|
||||||
"cSpell.enabled": false,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -585,3 +585,41 @@ In your Thymeleaf templates, use the `#{key}` syntax to reference the new transl
|
|||||||
```
|
```
|
||||||
|
|
||||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||||
|
|
||||||
|
|
||||||
|
## Managing Dependencies
|
||||||
|
|
||||||
|
When adding new dependencies or updating existing ones in Stirling-PDF, follow these steps to ensure proper verification and security:
|
||||||
|
|
||||||
|
1. Update the dependency in `build.gradle`:
|
||||||
|
```groovy
|
||||||
|
dependencies {
|
||||||
|
// Add or update your dependency
|
||||||
|
implementation "com.example:new-library:1.2.3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Generate new verification metadata and keys:
|
||||||
|
```bash
|
||||||
|
# Generate verification metadata with signatures and checksums
|
||||||
|
./gradlew clean dependencies buildEnvironment spotlessApply --write-verification-metadata sha256,pgp
|
||||||
|
|
||||||
|
# Export the .keys file
|
||||||
|
./gradlew --export-keys
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Files to commit:
|
||||||
|
- `build.gradle` - Your dependency changes
|
||||||
|
- `gradle/verification-metadata.xml` - Contains verification rules and checksums
|
||||||
|
- `gradle/verification-keyring.keys` - Contains PGP keys in text format
|
||||||
|
|
||||||
|
4. Verify the build works with the new verification:
|
||||||
|
```bash
|
||||||
|
./gradlew build
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Before committing, check:
|
||||||
|
- Verify any new BOM files are properly handled in verification metadata
|
||||||
|
- Review the changes in `verification-metadata.xml` to ensure they match your dependency updates
|
||||||
|
|
||||||
|
This ensures dependencies are properly verified and secure while maintaining transparency in the repository.
|
||||||
|
|||||||
71
Dockerfile
71
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -35,56 +35,45 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
PUID=1000 \
|
PUID=1000 \
|
||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022 \
|
UMASK=022
|
||||||
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
|
||||||
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
|
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
qpdf \
|
qpdf \
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
openssl-dev \
|
openssl-dev \
|
||||||
openjdk21-jre \
|
openjdk21-jre \
|
||||||
# Doc conversion
|
# Doc conversion
|
||||||
gcompat \
|
libreoffice \
|
||||||
libc6-compat \
|
# pdftohtml
|
||||||
libreoffice \
|
poppler-utils \
|
||||||
# pdftohtml
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
poppler-utils \
|
tesseract-ocr-data-eng \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
# CV
|
||||||
tesseract-ocr-data-eng \
|
py3-opencv \
|
||||||
# CV
|
# python3/pip
|
||||||
py3-opencv \
|
python3 \
|
||||||
python3 \
|
py3-pip && \
|
||||||
py3-pip \
|
# uno unoconv and HTML
|
||||||
py3-pillow@testing \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
py3-pdf2image@testing && \
|
|
||||||
python3 -m venv /opt/venv && \
|
|
||||||
export PATH="/opt/venv/bin:$PATH" && \
|
|
||||||
pip install --upgrade pip && \
|
|
||||||
pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
|
||||||
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
|
||||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
|
||||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
chmod +x /scripts/* && \
|
chmod +x /scripts/* && \
|
||||||
chmod +x /scripts/init.sh && \
|
chmod +x /scripts/init.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
@@ -93,4 +82,4 @@ EXPOSE 8080/tcp
|
|||||||
|
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -9,11 +9,10 @@ COPY . .
|
|||||||
|
|
||||||
# Build the application with DOCKER_ENABLE_SECURITY=false
|
# Build the application with DOCKER_ENABLE_SECURITY=false
|
||||||
RUN DOCKER_ENABLE_SECURITY=true \
|
RUN DOCKER_ENABLE_SECURITY=true \
|
||||||
STIRLING_PDF_DESKTOP_UI=false \
|
./gradlew clean build
|
||||||
./gradlew clean build
|
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -38,63 +37,52 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022 \
|
UMASK=022 \
|
||||||
FAT_DOCKER=true \
|
FAT_DOCKER=true \
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
|
|
||||||
UNO_PATH=/usr/lib/libreoffice/program \
|
|
||||||
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
|
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@community https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
openssl-dev \
|
openssl-dev \
|
||||||
openjdk21-jre \
|
openjdk21-jre \
|
||||||
# Doc conversion
|
# Doc conversion
|
||||||
gcompat \
|
libreoffice \
|
||||||
libc6-compat \
|
# pdftohtml
|
||||||
libreoffice \
|
poppler-utils \
|
||||||
# pdftohtml
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
poppler-utils \
|
qpdf \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
tesseract-ocr-data-eng \
|
||||||
qpdf \
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||||
tesseract-ocr-data-eng \
|
# CV
|
||||||
|
py3-opencv \
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
|
# python3/pip
|
||||||
# CV
|
python3 \
|
||||||
py3-opencv \
|
py3-pip && \
|
||||||
python3 \
|
# uno unoconv and HTML
|
||||||
py3-pip \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
py3-pillow@testing \
|
|
||||||
py3-pdf2image@testing && \
|
|
||||||
python3 -m venv /opt/venv && \
|
|
||||||
export PATH="/opt/venv/bin:$PATH" && \
|
|
||||||
pip install --upgrade pip && \
|
|
||||||
pip install --no-cache-dir --upgrade unoserver weasyprint && \
|
|
||||||
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
|
|
||||||
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
|
|
||||||
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
|
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
chmod +x /scripts/* && \
|
chmod +x /scripts/* && \
|
||||||
chmod +x /scripts/init.sh && \
|
chmod +x /scripts/init.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
FROM alpine:3.21.2@sha256:56fa17d2a7e7f168a043a2712e63aed1f8543aeafdcee47c58dcffe38ed51099
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ Fork Stirling-PDF and create a new branch out of `main`.
|
|||||||
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
|
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
|
||||||
|
|
||||||
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
|
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
|
||||||
|
- Add a flag SVG file to: [flags directory](https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags)
|
||||||
|
|
||||||
|
Any SVG flags are fine; most of the current ones were sourced from [here](https://flagicons.lipis.dev/). If your language isn't represented by a flag, choose a similar one, such as Saudi Arabia's flag for Arabic.
|
||||||
|
|
||||||
For example, to add Polish, you would add:
|
For example, to add Polish, you would add:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}" ></div>
|
<a th:if="${#lists.isEmpty(@languages) or #lists.contains(@languages, 'pl_PL')}" class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL"> <img th:src="@{'/images/flags/pl.svg'}" alt="icon" width="20" height="15"> Polski</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `data-bs-language-code` is the code used to reference the file in the next step.
|
The `data-bs-language-code` is the code used to reference the file in the next step.
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
[](https://discord.gg/HYmhKj45pU)
|
[](https://discord.gg/HYmhKj45pU)
|
||||||
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
|
[](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
|
|
||||||
@@ -119,40 +120,40 @@ Stirling-PDF currently supports 39 languages!
|
|||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|||||||
50
build.gradle
50
build.gradle
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.4.3"
|
id "org.springframework.boot" version "3.4.1"
|
||||||
id "io.spring.dependency-management" version "1.1.7"
|
id "io.spring.dependency-management" version "1.1.7"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
@@ -8,24 +8,24 @@ plugins {
|
|||||||
id "com.diffplug.spotless" version "7.0.2"
|
id "com.diffplug.spotless" version "7.0.2"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
//id "nebula.lint" version "19.0.3"
|
//id "nebula.lint" version "19.0.3"
|
||||||
id("org.panteleyev.jpackageplugin") version "1.6.1"
|
id("org.panteleyev.jpackageplugin") version "1.6.0"
|
||||||
id "org.sonarqube" version "6.0.1.5171"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.4.3"
|
springBootVersion = "3.4.1"
|
||||||
pdfboxVersion = "3.0.4"
|
pdfboxVersion = "3.0.4"
|
||||||
|
logbackVersion = "1.5.7"
|
||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.12.0"
|
||||||
lombokVersion = "1.18.36"
|
lombokVersion = "1.18.36"
|
||||||
bouncycastleVersion = "1.80"
|
bouncycastleVersion = "1.80"
|
||||||
springSecuritySamlVersion = "6.4.3"
|
springSecuritySamlVersion = "6.4.2"
|
||||||
openSamlVersion = "4.3.2"
|
openSamlVersion = "4.3.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.43.0"
|
version = "0.40.1"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
@@ -34,6 +34,7 @@ java {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url = "https://jitpack.io" }
|
||||||
maven { url = "https://build.shibboleth.net/maven/releases" }
|
maven { url = "https://build.shibboleth.net/maven/releases" }
|
||||||
maven { url = "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
|
maven { url = "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
|
||||||
}
|
}
|
||||||
@@ -260,7 +261,7 @@ spotless {
|
|||||||
|
|
||||||
googleJavaFormat("1.25.2").aosp().reorderImports(false)
|
googleJavaFormat("1.25.2").aosp().reorderImports(false)
|
||||||
|
|
||||||
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
|
importOrder("java", "javax", "org", "com", "net", "io")
|
||||||
toggleOffOn()
|
toggleOffOn()
|
||||||
trimTrailingWhitespace()
|
trimTrailingWhitespace()
|
||||||
leadingTabsToSpaces()
|
leadingTabsToSpaces()
|
||||||
@@ -268,17 +269,6 @@ spotless {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sonar {
|
|
||||||
properties {
|
|
||||||
property "sonar.projectKey", "Stirling-Tools_Stirling-PDF"
|
|
||||||
property "sonar.organization", "stirling-tools"
|
|
||||||
|
|
||||||
property "sonar.exclusions", "**/build-wrapper-dump.json, src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
|
||||||
property "sonar.coverage.exclusions", "src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
|
||||||
property "sonar.cpd.exclusions", "src/main/java/org/apache/**, src/main/resources/static/pdfjs/**, src/main/resources/static/pdfjs-legacy/**, src/main/resources/static/js/thirdParty/**"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//gradleLint {
|
//gradleLint {
|
||||||
// rules=['unused-dependency']
|
// rules=['unused-dependency']
|
||||||
// }
|
// }
|
||||||
@@ -293,26 +283,14 @@ configurations.all {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
//tmp for security bumps
|
|
||||||
implementation 'ch.qos.logback:logback-core:1.5.16'
|
|
||||||
implementation 'ch.qos.logback:logback-classic:1.5.16'
|
|
||||||
|
|
||||||
|
|
||||||
// Exclude vulnerable BouncyCastle version used in tableau
|
|
||||||
configurations.all {
|
|
||||||
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
|
|
||||||
exclude group: 'org.bouncycastle', module: 'bcutil-jdk15on'
|
|
||||||
exclude group: 'org.bouncycastle', module: 'bcmail-jdk15on'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
||||||
implementation "me.friwi:jcefmaven:132.3.1"
|
implementation "me.friwi:jcefmaven:127.3.1"
|
||||||
implementation "org.openjfx:javafx-controls:21"
|
implementation "org.openjfx:javafx-controls:21"
|
||||||
implementation "org.openjfx:javafx-swing:21"
|
implementation "org.openjfx:javafx-swing:21"
|
||||||
}
|
}
|
||||||
|
|
||||||
//security updates
|
//security updates
|
||||||
implementation "org.springframework:spring-webmvc:6.2.3"
|
implementation "org.springframework:spring-webmvc:6.2.2"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
||||||
|
|
||||||
@@ -331,8 +309,8 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.session:spring-session-core:3.4.2"
|
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
||||||
implementation "org.springframework:spring-jdbc:6.2.3"
|
implementation "org.springframework:spring-jdbc:6.2.2"
|
||||||
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
// Don't upgrade h2database
|
// Don't upgrade h2database
|
||||||
@@ -347,8 +325,8 @@ dependencies {
|
|||||||
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
||||||
implementation 'com.coveo:saml-client:5.0.0'
|
implementation 'com.coveo:saml-client:5.0.0'
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
|
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
|
||||||
@@ -407,7 +385,7 @@ dependencies {
|
|||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
implementation "io.micrometer:micrometer-core:1.14.4"
|
implementation "io.micrometer:micrometer-core:1.14.3"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.24.0"
|
implementation "org.commonmark:commonmark:0.24.0"
|
||||||
|
|||||||
BIN
docs/stirling-pdf.png
Normal file
BIN
docs/stirling-pdf.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
1
docs/stirling-transparent.svg
Normal file
1
docs/stirling-transparent.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.4 KiB |
BIN
gradle/verification-keyring.gpg
Normal file
BIN
gradle/verification-keyring.gpg
Normal file
Binary file not shown.
6785
gradle/verification-keyring.keys
Normal file
6785
gradle/verification-keyring.keys
Normal file
File diff suppressed because it is too large
Load Diff
4550
gradle/verification-metadata.xml
Normal file
4550
gradle/verification-metadata.xml
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 242 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 145 KiB |
@@ -6,7 +6,6 @@ import org.springframework.core.Ordered;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
|||||||
@@ -13,14 +13,12 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.posthog.java.shaded.org.json.JSONObject;
|
import com.posthog.java.shaded.org.json.JSONObject;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class KeygenLicenseVerifier {
|
public class KeygenLicenseVerifier {
|
||||||
// todo: place in config files?
|
|
||||||
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
||||||
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -69,7 +67,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error verifying license: {}", e.getMessage());
|
log.error("Error verifying license: " + e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,9 +94,10 @@ public class KeygenLicenseVerifier {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
log.debug("ValidateLicenseResponse body: {}", response.body());
|
log.debug(" validateLicenseResponse body: " + response.body());
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
|
|
||||||
JsonNode metaNode = jsonResponse.path("meta");
|
JsonNode metaNode = jsonResponse.path("meta");
|
||||||
boolean isValid = metaNode.path("valid").asBoolean();
|
boolean isValid = metaNode.path("valid").asBoolean();
|
||||||
|
|
||||||
@@ -120,7 +119,7 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info(applicationProperties.toString());
|
log.info(applicationProperties.toString());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.error("Error validating license. Status code: {}", response.statusCode());
|
log.error("Error validating license. Status code: " + response.statusCode());
|
||||||
}
|
}
|
||||||
return jsonResponse;
|
return jsonResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import org.springframework.scheduling.annotation.Scheduled;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ public class LicenseKeyChecker {
|
|||||||
|
|
||||||
public void updateLicenseKey(String newKey) throws IOException {
|
public void updateLicenseKey(String newKey) throws IOException {
|
||||||
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
||||||
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
|
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
|
||||||
checkLicense();
|
checkLicense();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.ServerSocket;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -21,14 +22,11 @@ import io.github.pixee.security.SystemCommand;
|
|||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
import stirling.software.SPDF.config.ConfigInitializer;
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@@ -64,12 +62,6 @@ public class SPDFApplication {
|
|||||||
app.setHeadless(false);
|
app.setHeadless(false);
|
||||||
props.put("java.awt.headless", "false");
|
props.put("java.awt.headless", "false");
|
||||||
props.put("spring.main.web-application-type", "servlet");
|
props.put("spring.main.web-application-type", "servlet");
|
||||||
|
|
||||||
int desiredPort = 8080;
|
|
||||||
String port = UrlUtils.findAvailablePort(desiredPort);
|
|
||||||
props.put("server.port", port);
|
|
||||||
System.setProperty("server.port", port);
|
|
||||||
log.info("Desktop UI mode: Using port {}", port);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setAdditionalProfiles(getActiveProfile(args));
|
app.setAdditionalProfiles(getActiveProfile(args));
|
||||||
@@ -83,18 +75,18 @@ public class SPDFApplication {
|
|||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
// External config files
|
// External config files
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
|
||||||
log.info("Settings file: {}", settingsPath.toString());
|
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
|
||||||
if (Files.exists(settingsPath)) {
|
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location", "file:" + settingsPath.toString());
|
"spring.config.additional-location",
|
||||||
|
"file:" + InstallationPathConfig.getSettingsPath());
|
||||||
} else {
|
} else {
|
||||||
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
|
log.warn(
|
||||||
|
"External configuration file '{}' does not exist.",
|
||||||
|
InstallationPathConfig.getSettingsPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
|
||||||
log.info("Custom settings file: {}", customSettingsPath.toString());
|
|
||||||
if (Files.exists(customSettingsPath)) {
|
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
if (!existingLocation.isEmpty()) {
|
if (!existingLocation.isEmpty()) {
|
||||||
@@ -102,11 +94,11 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existingLocation + "file:" + customSettingsPath.toString());
|
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Custom configuration file '{}' does not exist.",
|
"Custom configuration file '{}' does not exist.",
|
||||||
customSettingsPath.toString());
|
InstallationPathConfig.getCustomSettingsPath());
|
||||||
}
|
}
|
||||||
Properties finalProps = new Properties();
|
Properties finalProps = new Properties();
|
||||||
|
|
||||||
@@ -128,7 +120,7 @@ public class SPDFApplication {
|
|||||||
try {
|
try {
|
||||||
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
||||||
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating directories: {}", e.getMessage());
|
log.error("Error creating directories: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +149,7 @@ public class SPDFApplication {
|
|||||||
} else if (os.contains("nix") || os.contains("nux")) {
|
} else if (os.contains("nix") || os.contains("nux")) {
|
||||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
log.error("Error opening browser: {}", e.getMessage());
|
log.error("Error opening browser: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,17 +158,7 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Value("${server.port:8080}")
|
@Value("${server.port:8080}")
|
||||||
public void setServerPort(String port) {
|
public void setServerPortStatic(String port) {
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
|
||||||
SPDFApplication.serverPortStatic =
|
|
||||||
"0"; // This will let Spring Boot assign an available port
|
|
||||||
} else {
|
|
||||||
SPDFApplication.serverPortStatic = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setServerPortStatic(String port) {
|
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
if ("auto".equalsIgnoreCase(port)) {
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||||
SPDFApplication.serverPortStatic =
|
SPDFApplication.serverPortStatic =
|
||||||
@@ -213,11 +195,36 @@ public class SPDFApplication {
|
|||||||
return new String[] {"default"};
|
return new String[] {"default"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isPortAvailable(int port) {
|
||||||
|
try (ServerSocket socket = new ServerSocket(port)) {
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
||||||
|
private static String findAvailablePort(int startPort) {
|
||||||
|
int port = startPort;
|
||||||
|
while (!isPortAvailable(port)) {
|
||||||
|
port++;
|
||||||
|
}
|
||||||
|
return String.valueOf(port);
|
||||||
|
}
|
||||||
|
|
||||||
public static String getStaticBaseUrl() {
|
public static String getStaticBaseUrl() {
|
||||||
return baseUrlStatic;
|
return baseUrlStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNonStaticBaseUrl() {
|
||||||
|
return baseUrlStatic;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getStaticPort() {
|
public static String getStaticPort() {
|
||||||
return serverPortStatic;
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNonStaticPort() {
|
||||||
|
return serverPortStatic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,17 +34,13 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import me.friwi.jcefmaven.CefAppBuilder;
|
import me.friwi.jcefmaven.CefAppBuilder;
|
||||||
import me.friwi.jcefmaven.EnumProgress;
|
import me.friwi.jcefmaven.EnumProgress;
|
||||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||||
|
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.utils.UIScaling;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -219,7 +215,7 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
frame.setSize(1280, 768);
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
|
|
||||||
loadIcon();
|
loadIcon();
|
||||||
@@ -268,9 +264,7 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
frame.setOpacity(1.0f);
|
frame.setOpacity(1.0f);
|
||||||
frame.setUndecorated(false);
|
frame.setUndecorated(false);
|
||||||
frame.pack();
|
frame.pack();
|
||||||
frame.setSize(
|
frame.setSize(1280, 800);
|
||||||
UIScaling.scaleWidth(1280),
|
|
||||||
UIScaling.scaleHeight(800));
|
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
log.debug("Frame reconfigured");
|
log.debug("Frame reconfigured");
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
package stirling.software.SPDF.UI.impl;
|
package stirling.software.SPDF.UI.impl;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
|
|
||||||
import io.github.pixee.security.BoundedLineReader;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.UIScaling;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class LoadingWindow extends JDialog {
|
public class LoadingWindow extends JDialog {
|
||||||
private final JProgressBar progressBar;
|
private final JProgressBar progressBar;
|
||||||
@@ -25,13 +16,6 @@ public class LoadingWindow extends JDialog {
|
|||||||
private final JLabel brandLabel;
|
private final JLabel brandLabel;
|
||||||
private long startTime;
|
private long startTime;
|
||||||
|
|
||||||
private Timer stuckTimer;
|
|
||||||
private long stuckThreshold = 4000;
|
|
||||||
private long timeAt90Percent = -1;
|
|
||||||
private volatile Process explorerProcess;
|
|
||||||
private static final boolean IS_WINDOWS =
|
|
||||||
System.getProperty("os.name").toLowerCase().contains("win");
|
|
||||||
|
|
||||||
public LoadingWindow(Frame parent, String initialUrl) {
|
public LoadingWindow(Frame parent, String initialUrl) {
|
||||||
super(parent, "Initializing Stirling-PDF", true);
|
super(parent, "Initializing Stirling-PDF", true);
|
||||||
startTime = System.currentTimeMillis();
|
startTime = System.currentTimeMillis();
|
||||||
@@ -57,12 +41,12 @@ public class LoadingWindow extends JDialog {
|
|||||||
if (is != null) {
|
if (is != null) {
|
||||||
Image img = ImageIO.read(is);
|
Image img = ImageIO.read(is);
|
||||||
if (img != null) {
|
if (img != null) {
|
||||||
Image scaledImg = UIScaling.scaleIcon(img, 48, 48);
|
Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH);
|
||||||
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
|
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
|
||||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
gbc.gridy = 0;
|
gbc.gridy = 0;
|
||||||
mainPanel.add(iconLabel, gbc);
|
mainPanel.add(iconLabel, gbc);
|
||||||
log.info("Icon loaded and scaled successfully");
|
log.debug("Icon loaded and scaled successfully");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,8 +83,7 @@ public class LoadingWindow extends JDialog {
|
|||||||
setUndecorated(false);
|
setUndecorated(false);
|
||||||
|
|
||||||
// Set size and position
|
// Set size and position
|
||||||
setSize(UIScaling.scaleWidth(400), UIScaling.scaleHeight(200));
|
setSize(400, 200);
|
||||||
|
|
||||||
setLocationRelativeTo(parent);
|
setLocationRelativeTo(parent);
|
||||||
setAlwaysOnTop(true);
|
setAlwaysOnTop(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
@@ -111,163 +94,6 @@ public class LoadingWindow extends JDialog {
|
|||||||
System.currentTimeMillis() - startTime);
|
System.currentTimeMillis() - startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkAndRefreshExplorer() {
|
|
||||||
if (!IS_WINDOWS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (timeAt90Percent == -1) {
|
|
||||||
timeAt90Percent = System.currentTimeMillis();
|
|
||||||
stuckTimer =
|
|
||||||
new Timer(
|
|
||||||
1000,
|
|
||||||
e -> {
|
|
||||||
long currentTime = System.currentTimeMillis();
|
|
||||||
if (currentTime - timeAt90Percent > stuckThreshold) {
|
|
||||||
try {
|
|
||||||
log.debug(
|
|
||||||
"Attempting Windows explorer refresh due to 90% stuck state");
|
|
||||||
String currentDir = System.getProperty("user.dir");
|
|
||||||
|
|
||||||
// Store current explorer PIDs before we start new one
|
|
||||||
Set<String> existingPids = new HashSet<>();
|
|
||||||
ProcessBuilder listExplorer =
|
|
||||||
new ProcessBuilder(
|
|
||||||
"cmd",
|
|
||||||
"/c",
|
|
||||||
"wmic",
|
|
||||||
"process",
|
|
||||||
"where",
|
|
||||||
"name='explorer.exe'",
|
|
||||||
"get",
|
|
||||||
"ProcessId",
|
|
||||||
"/format:csv");
|
|
||||||
Process process = listExplorer.start();
|
|
||||||
BufferedReader reader =
|
|
||||||
new BufferedReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
process.getInputStream()));
|
|
||||||
String line;
|
|
||||||
while ((line =
|
|
||||||
BoundedLineReader.readLine(
|
|
||||||
reader, 5_000_000))
|
|
||||||
!= null) {
|
|
||||||
if (line.matches(".*\\d+.*")) { // Contains numbers
|
|
||||||
String[] parts = line.trim().split(",");
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
existingPids.add(
|
|
||||||
parts[parts.length - 1].trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
process.waitFor(2, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
// Start new explorer
|
|
||||||
ProcessBuilder pb =
|
|
||||||
new ProcessBuilder(
|
|
||||||
"cmd",
|
|
||||||
"/c",
|
|
||||||
"start",
|
|
||||||
"/min",
|
|
||||||
"/b",
|
|
||||||
"explorer.exe",
|
|
||||||
currentDir);
|
|
||||||
pb.redirectErrorStream(true);
|
|
||||||
explorerProcess = pb.start();
|
|
||||||
|
|
||||||
// Schedule cleanup
|
|
||||||
Timer cleanupTimer =
|
|
||||||
new Timer(
|
|
||||||
2000,
|
|
||||||
cleanup -> {
|
|
||||||
try {
|
|
||||||
// Find new explorer processes
|
|
||||||
ProcessBuilder findNewExplorer =
|
|
||||||
new ProcessBuilder(
|
|
||||||
"cmd",
|
|
||||||
"/c",
|
|
||||||
"wmic",
|
|
||||||
"process",
|
|
||||||
"where",
|
|
||||||
"name='explorer.exe'",
|
|
||||||
"get",
|
|
||||||
"ProcessId",
|
|
||||||
"/format:csv");
|
|
||||||
Process newProcess =
|
|
||||||
findNewExplorer.start();
|
|
||||||
BufferedReader newReader =
|
|
||||||
new BufferedReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
newProcess
|
|
||||||
.getInputStream()));
|
|
||||||
String newLine;
|
|
||||||
while ((newLine =
|
|
||||||
BoundedLineReader
|
|
||||||
.readLine(
|
|
||||||
newReader,
|
|
||||||
5_000_000))
|
|
||||||
!= null) {
|
|
||||||
if (newLine.matches(
|
|
||||||
".*\\d+.*")) {
|
|
||||||
String[] parts =
|
|
||||||
newLine.trim()
|
|
||||||
.split(",");
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
String pid =
|
|
||||||
parts[
|
|
||||||
parts.length
|
|
||||||
- 1]
|
|
||||||
.trim();
|
|
||||||
if (!existingPids
|
|
||||||
.contains(
|
|
||||||
pid)) {
|
|
||||||
log.debug(
|
|
||||||
"Found new explorer.exe with PID: "
|
|
||||||
+ pid);
|
|
||||||
ProcessBuilder
|
|
||||||
killProcess =
|
|
||||||
new ProcessBuilder(
|
|
||||||
"taskkill",
|
|
||||||
"/PID",
|
|
||||||
pid,
|
|
||||||
"/F");
|
|
||||||
killProcess
|
|
||||||
.redirectErrorStream(
|
|
||||||
true);
|
|
||||||
Process killResult =
|
|
||||||
killProcess
|
|
||||||
.start();
|
|
||||||
killResult.waitFor(
|
|
||||||
2,
|
|
||||||
TimeUnit
|
|
||||||
.SECONDS);
|
|
||||||
log.debug(
|
|
||||||
"Explorer process terminated: "
|
|
||||||
+ pid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newProcess.waitFor(
|
|
||||||
2, TimeUnit.SECONDS);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error(
|
|
||||||
"Error cleaning up Windows explorer process",
|
|
||||||
ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cleanupTimer.setRepeats(false);
|
|
||||||
cleanupTimer.start();
|
|
||||||
stuckTimer.stop();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Error refreshing Windows explorer", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stuckTimer.setRepeats(true);
|
|
||||||
stuckTimer.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(final int progress) {
|
public void setProgress(final int progress) {
|
||||||
SwingUtilities.invokeLater(
|
SwingUtilities.invokeLater(
|
||||||
() -> {
|
() -> {
|
||||||
@@ -289,23 +115,11 @@ public class LoadingWindow extends JDialog {
|
|||||||
|
|
||||||
// Add thread state logging
|
// Add thread state logging
|
||||||
Thread currentThread = Thread.currentThread();
|
Thread currentThread = Thread.currentThread();
|
||||||
log.info(
|
log.debug(
|
||||||
"Current thread state - Name: {}, State: {}, Priority: {}",
|
"Current thread state - Name: {}, State: {}, Priority: {}",
|
||||||
currentThread.getName(),
|
currentThread.getName(),
|
||||||
currentThread.getState(),
|
currentThread.getState(),
|
||||||
currentThread.getPriority());
|
currentThread.getPriority());
|
||||||
|
|
||||||
if (validProgress >= 90 && validProgress < 95) {
|
|
||||||
checkAndRefreshExplorer();
|
|
||||||
} else {
|
|
||||||
// Reset the timer if we move past 95%
|
|
||||||
if (validProgress >= 95) {
|
|
||||||
if (stuckTimer != null) {
|
|
||||||
stuckTimer.stop();
|
|
||||||
}
|
|
||||||
timeAt90Percent = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar.setValue(validProgress);
|
progressBar.setValue(validProgress);
|
||||||
@@ -331,7 +145,7 @@ public class LoadingWindow extends JDialog {
|
|||||||
statusLabel.setText(validStatus);
|
statusLabel.setText(validStatus);
|
||||||
|
|
||||||
// Log UI state when status changes
|
// Log UI state when status changes
|
||||||
log.info(
|
log.debug(
|
||||||
"UI State - Window visible: {}, Progress: {}%, Status: {}",
|
"UI State - Window visible: {}, Progress: {}%, Status: {}",
|
||||||
isVisible(), progressBar.getValue(), validStatus);
|
isVisible(), progressBar.getValue(), validStatus);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import org.springframework.core.io.ResourceLoader;
|
|||||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -35,7 +34,10 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
|
name = "system.customHTMLFiles",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||||
@@ -96,9 +98,9 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "rateLimit")
|
@Bean(name = "rateLimit")
|
||||||
public boolean rateLimit() {
|
public boolean rateLimit() {
|
||||||
String rateLimit = System.getProperty("rateLimit");
|
String appName = System.getProperty("rateLimit");
|
||||||
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
if (appName == null) appName = System.getenv("rateLimit");
|
||||||
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
return (appName != null) ? Boolean.valueOf(appName) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "RunningInDocker")
|
@Bean(name = "RunningInDocker")
|
||||||
@@ -125,9 +127,18 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "bookAndHtmlFormatsInstalled")
|
||||||
|
public boolean bookAndHtmlFormatsInstalled() {
|
||||||
|
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||||
|
if (installOps == null) {
|
||||||
|
installOps = System.getenv("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||||
|
}
|
||||||
|
return "true".equalsIgnoreCase(installOps);
|
||||||
|
}
|
||||||
|
|
||||||
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||||
@Bean(name = "activeSecurity")
|
@Bean(name = "activSecurity")
|
||||||
public boolean missingActiveSecurity() {
|
public boolean missingActivSecurity() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,14 +181,16 @@ public class AppConfig {
|
|||||||
@Bean(name = "analyticsPrompt")
|
@Bean(name = "analyticsPrompt")
|
||||||
@Scope("request")
|
@Scope("request")
|
||||||
public boolean analyticsPrompt() {
|
public boolean analyticsPrompt() {
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() == null;
|
return applicationProperties.getSystem().getEnableAnalytics() == null
|
||||||
|
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "analyticsEnabled")
|
@Bean(name = "analyticsEnabled")
|
||||||
@Scope("request")
|
@Scope("request")
|
||||||
public boolean analyticsEnabled() {
|
public boolean analyticsEnabled() {
|
||||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
||||||
return applicationProperties.getSystem().isAnalyticsEnabled();
|
return applicationProperties.getSystem().getEnableAnalytics() != null
|
||||||
|
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "StirlingPDFLabel")
|
@Bean(name = "StirlingPDFLabel")
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package stirling.software.SPDF.config;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Service
|
||||||
class AppUpdateService {
|
class AppUpdateService {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
"endpoints",
|
"endpoints",
|
||||||
"logout",
|
"logout",
|
||||||
"error",
|
"error",
|
||||||
"errorOAuth",
|
"erroroauth",
|
||||||
"file",
|
"file",
|
||||||
"messageType",
|
"messageType",
|
||||||
"infoMessage");
|
"infoMessage");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ public class ConfigInitializer {
|
|||||||
log.info("Created settings file from template");
|
log.info("Created settings file from template");
|
||||||
} else {
|
} else {
|
||||||
// 2) Merge existing file with the template
|
// 2) Merge existing file with the template
|
||||||
|
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
|
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
|
||||||
if (templateResource == null) {
|
if (templateResource == null) {
|
||||||
throw new IOException("Resource not found: settings.yml.template");
|
throw new IOException("Resource not found: settings.yml.template");
|
||||||
@@ -47,33 +49,160 @@ public class ConfigInitializer {
|
|||||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy setting.yaml to a temp location so we can read lines
|
// 2a) Read lines from both files
|
||||||
Path settingTempPath = Files.createTempFile("settings", ".yaml");
|
List<String> templateLines = Files.readAllLines(tempTemplatePath);
|
||||||
try (InputStream in = Files.newInputStream(destPath)) {
|
List<String> mainLines = Files.readAllLines(settingsPath);
|
||||||
Files.copy(in, settingTempPath, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
}
|
|
||||||
|
|
||||||
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
|
// 2b) Merge lines
|
||||||
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
List<String> mergedLines = mergeYamlLinesWithTemplate(templateLines, mainLines);
|
||||||
|
|
||||||
boolean changesMade =
|
// 2c) Only write if there's an actual difference
|
||||||
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
if (!mergedLines.equals(mainLines)) {
|
||||||
if (changesMade) {
|
Files.write(settingsPath, mergedLines);
|
||||||
settingsTemplateFile.save(destPath);
|
|
||||||
log.info("Settings file updated based on template changes.");
|
log.info("Settings file updated based on template changes.");
|
||||||
} else {
|
} else {
|
||||||
log.info("No changes detected; settings file left as-is.");
|
log.info("No changes detected; settings file left as-is.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.deleteIfExists(tempTemplatePath);
|
Files.deleteIfExists(tempTemplatePath);
|
||||||
Files.deleteIfExists(settingTempPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Ensure custom settings file exists
|
// 3) Ensure custom settings file exists
|
||||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||||
if (Files.notExists(customSettingsPath)) {
|
if (!Files.exists(customSettingsPath)) {
|
||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
log.info("Created custom_settings file: {}", customSettingsPath.toString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge logic that: - Reads the template lines block-by-block (where a "block" = a key and all
|
||||||
|
* the lines that belong to it), - If the main file has that key, we keep the main file's block
|
||||||
|
* (preserving whitespace + inline comments). - Otherwise, we insert the template's block. - We
|
||||||
|
* also remove keys from main that no longer exist in the template.
|
||||||
|
*
|
||||||
|
* @param templateLines lines from settings.yml.template
|
||||||
|
* @param mainLines lines from the existing settings.yml
|
||||||
|
* @return merged lines
|
||||||
|
*/
|
||||||
|
private List<String> mergeYamlLinesWithTemplate(
|
||||||
|
List<String> templateLines, List<String> mainLines) {
|
||||||
|
|
||||||
|
// 1) Parse template lines into an ordered map: path -> Block
|
||||||
|
LinkedHashMap<String, Block> templateBlocks = parseYamlBlocks(templateLines);
|
||||||
|
|
||||||
|
// 2) Parse main lines into a map: path -> Block
|
||||||
|
LinkedHashMap<String, Block> mainBlocks = parseYamlBlocks(mainLines);
|
||||||
|
|
||||||
|
// 3) Build the final list by iterating template blocks in order
|
||||||
|
List<String> merged = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, Block> entry : templateBlocks.entrySet()) {
|
||||||
|
String path = entry.getKey();
|
||||||
|
Block templateBlock = entry.getValue();
|
||||||
|
|
||||||
|
if (mainBlocks.containsKey(path)) {
|
||||||
|
// If main has the same block, prefer main's lines
|
||||||
|
merged.addAll(mainBlocks.get(path).lines);
|
||||||
|
} else {
|
||||||
|
// Otherwise, add the template block
|
||||||
|
merged.addAll(templateBlock.lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a list of lines into a map of "path -> Block" where "Block" is all lines that belong to
|
||||||
|
* that key (including subsequent indented lines). Very naive approach that may not work with
|
||||||
|
* advanced YAML.
|
||||||
|
*/
|
||||||
|
private LinkedHashMap<String, Block> parseYamlBlocks(List<String> lines) {
|
||||||
|
LinkedHashMap<String, Block> blocks = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
Block currentBlock = null;
|
||||||
|
String currentPath = null;
|
||||||
|
|
||||||
|
for (String line : lines) {
|
||||||
|
if (isLikelyKeyLine(line)) {
|
||||||
|
// Found a new "key: ..." line
|
||||||
|
if (currentBlock != null && currentPath != null) {
|
||||||
|
blocks.put(currentPath, currentBlock);
|
||||||
|
}
|
||||||
|
currentBlock = new Block();
|
||||||
|
currentBlock.lines.add(line);
|
||||||
|
currentPath = computePathForLine(line);
|
||||||
|
} else {
|
||||||
|
// Continuation of current block (comments, blank lines, sub-lines)
|
||||||
|
if (currentBlock == null) {
|
||||||
|
// If file starts with comments/blank lines, treat as "header block" with path
|
||||||
|
// ""
|
||||||
|
currentBlock = new Block();
|
||||||
|
currentPath = "";
|
||||||
|
}
|
||||||
|
currentBlock.lines.add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBlock != null && currentPath != null) {
|
||||||
|
blocks.put(currentPath, currentBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the line is likely "key:" or "key: value", ignoring comments/blank. Skips lines
|
||||||
|
* starting with "-" or "#".
|
||||||
|
*/
|
||||||
|
private boolean isLikelyKeyLine(String line) {
|
||||||
|
String trimmed = line.trim();
|
||||||
|
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int colonIdx = trimmed.indexOf(':');
|
||||||
|
return (colonIdx > 0); // someKey:
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a line like "security: ", returns "security" or "security.enableLogin"
|
||||||
|
// by looking at indentation. Very naive.
|
||||||
|
private static final Deque<String> pathStack = new ArrayDeque<>();
|
||||||
|
private static int currentIndentLevel = 0;
|
||||||
|
|
||||||
|
private String computePathForLine(String line) {
|
||||||
|
// count leading spaces
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (char c : line.toCharArray()) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
// assume 2 spaces = 1 indent
|
||||||
|
int indentLevel = leadingSpaces / 2;
|
||||||
|
|
||||||
|
String trimmed = line.trim();
|
||||||
|
int colonIdx = trimmed.indexOf(':');
|
||||||
|
String keyName = trimmed.substring(0, colonIdx).trim();
|
||||||
|
|
||||||
|
// pop stack until we match the new indent level
|
||||||
|
while (currentIndentLevel >= indentLevel && !pathStack.isEmpty()) {
|
||||||
|
pathStack.pop();
|
||||||
|
currentIndentLevel--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push the new key
|
||||||
|
pathStack.push(keyName);
|
||||||
|
currentIndentLevel = indentLevel;
|
||||||
|
|
||||||
|
// build path by reversing the stack
|
||||||
|
String[] arr = pathStack.toArray(new String[0]);
|
||||||
|
List<String> reversed = Arrays.asList(arr);
|
||||||
|
Collections.reverse(reversed);
|
||||||
|
return String.join(".", reversed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple holder for the lines that comprise a "block" (i.e. a key and its subsequent lines).
|
||||||
|
*/
|
||||||
|
private static class Block {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -8,24 +9,30 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
||||||
public class EndpointConfiguration {
|
public class EndpointConfiguration {
|
||||||
|
|
||||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
public EndpointConfiguration(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
init();
|
init();
|
||||||
processEnvironmentConfigs();
|
processEnvironmentConfigs();
|
||||||
}
|
}
|
||||||
@@ -190,8 +197,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
|
|
||||||
// Unoconvert
|
// Unoconv
|
||||||
addEndpointToGroup("Unoconvert", "file-to-pdf");
|
addEndpointToGroup("Unoconv", "file-to-pdf");
|
||||||
|
|
||||||
// qpdf
|
// qpdf
|
||||||
addEndpointToGroup("qpdf", "compress-pdf");
|
addEndpointToGroup("qpdf", "compress-pdf");
|
||||||
@@ -258,6 +265,9 @@ public class EndpointConfiguration {
|
|||||||
// Pdftohtml dependent endpoints
|
// Pdftohtml dependent endpoints
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
addEndpointToGroup("Pdftohtml", "pdf-to-markdown");
|
||||||
|
|
||||||
|
// disabled for now while we resolve issues
|
||||||
|
disableEndpoint("pdf-to-pdfa");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
@@ -265,6 +275,12 @@ public class EndpointConfiguration {
|
|||||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||||
|
|
||||||
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
|
if (groupsToRemove == null) {
|
||||||
|
groupsToRemove = new ArrayList<>();
|
||||||
|
}
|
||||||
|
groupsToRemove.add("Calibre");
|
||||||
|
}
|
||||||
if (endpointsToRemove != null) {
|
if (endpointsToRemove != null) {
|
||||||
for (String endpoint : endpointsToRemove) {
|
for (String endpoint : endpointsToRemove) {
|
||||||
disableEndpoint(endpoint.trim());
|
disableEndpoint(endpoint.trim());
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import java.util.stream.Collectors;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -17,29 +16,21 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class ExternalAppDepConfig {
|
public class ExternalAppDepConfig {
|
||||||
|
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
private final Map<String, List<String>> commandToGroupMapping =
|
||||||
|
new HashMap<>() {
|
||||||
|
|
||||||
private final String weasyprintPath;
|
{
|
||||||
private final String unoconvPath;
|
put("soffice", List.of("LibreOffice"));
|
||||||
private final Map<String, List<String>> commandToGroupMapping;
|
put("weasyprint", List.of("Weasyprint"));
|
||||||
|
put("pdftohtml", List.of("Pdftohtml"));
|
||||||
|
put("unoconv", List.of("Unoconv"));
|
||||||
|
put("qpdf", List.of("qpdf"));
|
||||||
|
put("tesseract", List.of("tesseract"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public ExternalAppDepConfig(
|
public ExternalAppDepConfig(EndpointConfiguration endpointConfiguration) {
|
||||||
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
|
||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
|
||||||
unoconvPath = runtimePathConfig.getUnoConvertPath();
|
|
||||||
|
|
||||||
commandToGroupMapping =
|
|
||||||
new HashMap<>() {
|
|
||||||
|
|
||||||
{
|
|
||||||
put("soffice", List.of("LibreOffice"));
|
|
||||||
put(weasyprintPath, List.of("Weasyprint"));
|
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
|
||||||
put(unoconvPath, List.of("Unoconvert"));
|
|
||||||
put("qpdf", List.of("qpdf"));
|
|
||||||
put("tesseract", List.of("tesseract"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCommandAvailable(String command) {
|
private boolean isCommandAvailable(String command) {
|
||||||
@@ -110,9 +101,9 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup("tesseract");
|
checkDependencyAndDisableGroup("tesseract");
|
||||||
checkDependencyAndDisableGroup("soffice");
|
checkDependencyAndDisableGroup("soffice");
|
||||||
checkDependencyAndDisableGroup("qpdf");
|
checkDependencyAndDisableGroup("qpdf");
|
||||||
checkDependencyAndDisableGroup(weasyprintPath);
|
checkDependencyAndDisableGroup("weasyprint");
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup(unoconvPath);
|
checkDependencyAndDisableGroup("unoconv");
|
||||||
// Special handling for Python/OpenCV dependencies
|
// Special handling for Python/OpenCV dependencies
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||||
if (!pythonAvailable) {
|
if (!pythonAvailable) {
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import org.springframework.stereotype.Component;
|
|||||||
import io.micrometer.common.util.StringUtils;
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@@ -44,7 +42,7 @@ public class InitialSetup {
|
|||||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||||
// Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
uuid = UUID.randomUUID().toString();
|
uuid = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.UUID", uuid);
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
||||||
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +52,7 @@ public class InitialSetup {
|
|||||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
if (!GeneralUtils.isValidUUID(secretKey)) {
|
||||||
// Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
secretKey = UUID.randomUUID().toString();
|
secretKey = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.key", secretKey);
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
||||||
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +62,8 @@ public class InitialSetup {
|
|||||||
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
||||||
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
||||||
if (!csrf) {
|
if (!csrf) {
|
||||||
GeneralUtils.saveKeyToSettings("security.csrfDisabled", false);
|
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
|
||||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
|
||||||
applicationProperties.getSecurity().setCsrfDisabled(false);
|
applicationProperties.getSecurity().setCsrfDisabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,14 +74,14 @@ public class InitialSetup {
|
|||||||
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
||||||
if (StringUtils.isEmpty(termsUrl)) {
|
if (StringUtils.isEmpty(termsUrl)) {
|
||||||
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
||||||
GeneralUtils.saveKeyToSettings("legal.termsAndConditions", defaultTermsUrl);
|
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
|
||||||
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
||||||
}
|
}
|
||||||
// Initialize Privacy Policy
|
// Initialize Privacy Policy
|
||||||
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
||||||
if (StringUtils.isEmpty(privacyUrl)) {
|
if (StringUtils.isEmpty(privacyUrl)) {
|
||||||
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
||||||
GeneralUtils.saveKeyToSettings("legal.privacyPolicy", defaultPrivacyUrl);
|
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
|
||||||
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +95,7 @@ public class InitialSetup {
|
|||||||
appVersion = props.getProperty("version");
|
appVersion = props.getProperty("version");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion);
|
|
||||||
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
||||||
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -12,6 +11,7 @@ public class InstallationPathConfig {
|
|||||||
// Root paths
|
// Root paths
|
||||||
private static final String LOG_PATH;
|
private static final String LOG_PATH;
|
||||||
private static final String CONFIG_PATH;
|
private static final String CONFIG_PATH;
|
||||||
|
private static final String PIPELINE_PATH;
|
||||||
private static final String CUSTOM_FILES_PATH;
|
private static final String CUSTOM_FILES_PATH;
|
||||||
private static final String CLIENT_WEBUI_PATH;
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
|
|
||||||
@@ -19,6 +19,11 @@ public class InstallationPathConfig {
|
|||||||
private static final String SETTINGS_PATH;
|
private static final String SETTINGS_PATH;
|
||||||
private static final String CUSTOM_SETTINGS_PATH;
|
private static final String CUSTOM_SETTINGS_PATH;
|
||||||
|
|
||||||
|
// Pipeline paths
|
||||||
|
private static final String PIPELINE_WATCHED_FOLDERS_PATH;
|
||||||
|
private static final String PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
|
private static final String PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
||||||
|
|
||||||
// Custom file paths
|
// Custom file paths
|
||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
private static final String TEMPLATES_PATH;
|
private static final String TEMPLATES_PATH;
|
||||||
@@ -30,6 +35,7 @@ public class InstallationPathConfig {
|
|||||||
// Initialize root paths
|
// Initialize root paths
|
||||||
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
||||||
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
||||||
|
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
|
||||||
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
||||||
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
||||||
|
|
||||||
@@ -37,6 +43,11 @@ public class InstallationPathConfig {
|
|||||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||||
|
|
||||||
|
// Initialize pipeline paths
|
||||||
|
PIPELINE_WATCHED_FOLDERS_PATH = PIPELINE_PATH + "watchedFolders" + File.separator;
|
||||||
|
PIPELINE_FINISHED_FOLDERS_PATH = PIPELINE_PATH + "finishedFolders" + File.separator;
|
||||||
|
PIPELINE_DEFAULT_WEB_UI_CONFIGS = PIPELINE_PATH + "defaultWebUIConfigs" + File.separator;
|
||||||
|
|
||||||
// Initialize custom file paths
|
// Initialize custom file paths
|
||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||||
@@ -47,29 +58,26 @@ public class InstallationPathConfig {
|
|||||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
if (os.contains("win")) {
|
if (os.contains("win")) {
|
||||||
return Paths.get(
|
return System.getenv("APPDATA") + File.separator + "Stirling-PDF" + File.separator;
|
||||||
System.getenv("APPDATA"), // parent path
|
|
||||||
"Stirling-PDF")
|
|
||||||
.toString()
|
|
||||||
+ File.separator;
|
|
||||||
} else if (os.contains("mac")) {
|
} else if (os.contains("mac")) {
|
||||||
return Paths.get(
|
return System.getProperty("user.home")
|
||||||
System.getProperty("user.home"),
|
+ File.separator
|
||||||
"Library",
|
+ "Library"
|
||||||
"Application Support",
|
+ File.separator
|
||||||
"Stirling-PDF")
|
+ "Application Support"
|
||||||
.toString()
|
+ File.separator
|
||||||
|
+ "Stirling-PDF"
|
||||||
+ File.separator;
|
+ File.separator;
|
||||||
} else {
|
} else {
|
||||||
return Paths.get(
|
return System.getProperty("user.home")
|
||||||
System.getProperty("user.home"), // parent path
|
+ File.separator
|
||||||
".config",
|
+ ".config"
|
||||||
"Stirling-PDF")
|
+ File.separator
|
||||||
.toString()
|
+ "Stirling-PDF"
|
||||||
+ File.separator;
|
+ File.separator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "." + File.separator;
|
return "./";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPath() {
|
public static String getPath() {
|
||||||
@@ -84,6 +92,10 @@ public class InstallationPathConfig {
|
|||||||
return CONFIG_PATH;
|
return CONFIG_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getPipelinePath() {
|
||||||
|
return PIPELINE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getCustomFilesPath() {
|
public static String getCustomFilesPath() {
|
||||||
return CUSTOM_FILES_PATH;
|
return CUSTOM_FILES_PATH;
|
||||||
}
|
}
|
||||||
@@ -100,6 +112,18 @@ public class InstallationPathConfig {
|
|||||||
return CUSTOM_SETTINGS_PATH;
|
return CUSTOM_SETTINGS_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getPipelineWatchedFoldersDir() {
|
||||||
|
return PIPELINE_WATCHED_FOLDERS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPipelineFinishedFoldersDir() {
|
||||||
|
return PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPipelineDefaultWebUIConfigsDir() {
|
||||||
|
return PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
||||||
|
}
|
||||||
|
|
||||||
public static String getStaticPath() {
|
public static String getStaticPath() {
|
||||||
return STATIC_PATH;
|
return STATIC_PATH;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import com.posthog.java.PostHog;
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.CustomPaths.Operations;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.CustomPaths.Pipeline;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
@Getter
|
|
||||||
public class RuntimePathConfig {
|
|
||||||
private final ApplicationProperties properties;
|
|
||||||
private final String basePath;
|
|
||||||
private final String weasyPrintPath;
|
|
||||||
private final String unoConvertPath;
|
|
||||||
|
|
||||||
// Pipeline paths
|
|
||||||
private final String pipelineWatchedFoldersPath;
|
|
||||||
private final String pipelineFinishedFoldersPath;
|
|
||||||
private final String pipelineDefaultWebUiConfigs;
|
|
||||||
private final String pipelinePath;
|
|
||||||
|
|
||||||
public RuntimePathConfig(ApplicationProperties properties) {
|
|
||||||
this.properties = properties;
|
|
||||||
this.basePath = InstallationPathConfig.getPath();
|
|
||||||
|
|
||||||
this.pipelinePath = Path.of(basePath, "pipeline").toString();
|
|
||||||
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
|
|
||||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
|
||||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
|
||||||
|
|
||||||
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
|
|
||||||
|
|
||||||
this.pipelineWatchedFoldersPath =
|
|
||||||
resolvePath(
|
|
||||||
defaultWatchedFolders,
|
|
||||||
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
|
|
||||||
this.pipelineFinishedFoldersPath =
|
|
||||||
resolvePath(
|
|
||||||
defaultFinishedFolders,
|
|
||||||
pipeline != null ? pipeline.getFinishedFoldersDir() : null);
|
|
||||||
this.pipelineDefaultWebUiConfigs =
|
|
||||||
resolvePath(
|
|
||||||
defaultWebUIConfigs,
|
|
||||||
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
|
|
||||||
|
|
||||||
boolean isDocker = isRunningInDocker();
|
|
||||||
|
|
||||||
// Initialize Operation paths
|
|
||||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
|
||||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
|
||||||
|
|
||||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
|
||||||
this.weasyPrintPath =
|
|
||||||
resolvePath(
|
|
||||||
defaultWeasyPrintPath,
|
|
||||||
operations != null ? operations.getWeasyprint() : null);
|
|
||||||
this.unoConvertPath =
|
|
||||||
resolvePath(
|
|
||||||
defaultUnoConvertPath,
|
|
||||||
operations != null ? operations.getUnoconvert() : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String resolvePath(String defaultPath, String customPath) {
|
|
||||||
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRunningInDocker() {
|
|
||||||
return Files.exists(Path.of("/.dockerenv"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,479 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import org.snakeyaml.engine.v2.api.Dump;
|
|
||||||
import org.snakeyaml.engine.v2.api.DumpSettings;
|
|
||||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
|
||||||
import org.snakeyaml.engine.v2.api.StreamDataWriter;
|
|
||||||
import org.snakeyaml.engine.v2.common.FlowStyle;
|
|
||||||
import org.snakeyaml.engine.v2.common.ScalarStyle;
|
|
||||||
import org.snakeyaml.engine.v2.composer.Composer;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.MappingNode;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.Node;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.NodeTuple;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.ScalarNode;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.SequenceNode;
|
|
||||||
import org.snakeyaml.engine.v2.nodes.Tag;
|
|
||||||
import org.snakeyaml.engine.v2.parser.ParserImpl;
|
|
||||||
import org.snakeyaml.engine.v2.scanner.StreamReader;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class YamlHelper {
|
|
||||||
|
|
||||||
// YAML dump settings with comment support and block flow style
|
|
||||||
private static final DumpSettings DUMP_SETTINGS =
|
|
||||||
DumpSettings.builder()
|
|
||||||
.setDumpComments(true)
|
|
||||||
.setWidth(Integer.MAX_VALUE)
|
|
||||||
.setDefaultFlowStyle(FlowStyle.BLOCK)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private final String yamlContent; // Stores the entire YAML content as a string
|
|
||||||
|
|
||||||
private LoadSettings loadSettings =
|
|
||||||
LoadSettings.builder()
|
|
||||||
.setUseMarks(true)
|
|
||||||
.setMaxAliasesForCollections(Integer.MAX_VALUE)
|
|
||||||
.setAllowRecursiveKeys(true)
|
|
||||||
.setParseComments(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
private Path originalFilePath;
|
|
||||||
private Node updatedRootNode;
|
|
||||||
|
|
||||||
// Constructor with custom LoadSettings and YAML string
|
|
||||||
public YamlHelper(LoadSettings loadSettings, String yamlContent) {
|
|
||||||
this.loadSettings = loadSettings;
|
|
||||||
this.yamlContent = yamlContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructor that reads YAML from a file path
|
|
||||||
public YamlHelper(Path originalFilePath) throws IOException {
|
|
||||||
this.yamlContent = Files.readString(originalFilePath);
|
|
||||||
this.originalFilePath = originalFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates values in the target YAML based on values from the source YAML. It ensures that only
|
|
||||||
* existing keys in the target YAML are updated.
|
|
||||||
*
|
|
||||||
* @return true if at least one key was updated, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean updateValuesFromYaml(YamlHelper sourceYaml, YamlHelper targetYaml) {
|
|
||||||
boolean updated = false;
|
|
||||||
Set<String> sourceKeys = sourceYaml.getAllKeys();
|
|
||||||
Set<String> targetKeys = targetYaml.getAllKeys();
|
|
||||||
|
|
||||||
for (String key : sourceKeys) {
|
|
||||||
String[] keyArray = key.split("\\.");
|
|
||||||
|
|
||||||
Object newValue = sourceYaml.getValueByExactKeyPath(keyArray);
|
|
||||||
Object currentValue = targetYaml.getValueByExactKeyPath(keyArray);
|
|
||||||
if (newValue != null
|
|
||||||
&& (!newValue.equals(currentValue) || !sourceKeys.equals(targetKeys))) {
|
|
||||||
boolean updatedKey = targetYaml.updateValue(Arrays.asList(keyArray), newValue);
|
|
||||||
if (updatedKey) updated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a value in the YAML structure.
|
|
||||||
*
|
|
||||||
* @param keys The hierarchical keys leading to the value.
|
|
||||||
* @param newValue The new value to set.
|
|
||||||
* @return true if the value was updated, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean updateValue(List<String> keys, Object newValue) {
|
|
||||||
return updateValue(getRootNode(), keys, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean updateValue(Node node, List<String> keys, Object newValue) {
|
|
||||||
if (!(node instanceof MappingNode mappingNode)) return false;
|
|
||||||
|
|
||||||
List<NodeTuple> updatedTuples = new ArrayList<>();
|
|
||||||
boolean updated = false;
|
|
||||||
|
|
||||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
|
||||||
ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null;
|
|
||||||
if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) {
|
|
||||||
updatedTuples.add(tuple);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Node valueNode = tuple.getValueNode();
|
|
||||||
|
|
||||||
if (keys.size() == 1) {
|
|
||||||
Tag tag = valueNode.getTag();
|
|
||||||
Node newValueNode = null;
|
|
||||||
|
|
||||||
if (isAnyInteger(newValue)) {
|
|
||||||
newValueNode =
|
|
||||||
new ScalarNode(Tag.INT, String.valueOf(newValue), ScalarStyle.PLAIN);
|
|
||||||
} else if (isFloat(newValue)) {
|
|
||||||
Object floatValue = Float.valueOf(String.valueOf(newValue));
|
|
||||||
newValueNode =
|
|
||||||
new ScalarNode(
|
|
||||||
Tag.FLOAT, String.valueOf(floatValue), ScalarStyle.PLAIN);
|
|
||||||
} else if ("true".equals(newValue) || "false".equals(newValue)) {
|
|
||||||
newValueNode =
|
|
||||||
new ScalarNode(Tag.BOOL, String.valueOf(newValue), ScalarStyle.PLAIN);
|
|
||||||
} else if (newValue instanceof List<?> list) {
|
|
||||||
List<Node> sequenceNodes = new ArrayList<>();
|
|
||||||
for (Object item : list) {
|
|
||||||
Object obj = String.valueOf(item);
|
|
||||||
if (isAnyInteger(item)) {
|
|
||||||
tag = Tag.INT;
|
|
||||||
} else if (isFloat(item)) {
|
|
||||||
obj = Float.valueOf(String.valueOf(item));
|
|
||||||
tag = Tag.FLOAT;
|
|
||||||
} else if ("true".equals(item) || "false".equals(item)) {
|
|
||||||
tag = Tag.BOOL;
|
|
||||||
} else if (item == null || "null".equals(item)) {
|
|
||||||
tag = Tag.NULL;
|
|
||||||
} else {
|
|
||||||
tag = Tag.STR;
|
|
||||||
}
|
|
||||||
sequenceNodes.add(
|
|
||||||
new ScalarNode(tag, String.valueOf(obj), ScalarStyle.PLAIN));
|
|
||||||
}
|
|
||||||
newValueNode = new SequenceNode(Tag.SEQ, sequenceNodes, FlowStyle.FLOW);
|
|
||||||
} else if (tag == Tag.NULL) {
|
|
||||||
if ("true".equals(newValue)
|
|
||||||
|| "false".equals(newValue)
|
|
||||||
|| newValue instanceof Boolean) {
|
|
||||||
tag = Tag.BOOL;
|
|
||||||
}
|
|
||||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
|
||||||
} else {
|
|
||||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
|
||||||
}
|
|
||||||
copyComments(valueNode, newValueNode);
|
|
||||||
|
|
||||||
updatedTuples.add(new NodeTuple(keyNode, newValueNode));
|
|
||||||
updated = true;
|
|
||||||
} else if (valueNode instanceof MappingNode) {
|
|
||||||
updated = updateValue(valueNode, keys.subList(1, keys.size()), newValue);
|
|
||||||
updatedTuples.add(tuple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
mappingNode.getValue().clear();
|
|
||||||
mappingNode.getValue().addAll(updatedTuples);
|
|
||||||
}
|
|
||||||
setNewNode(node);
|
|
||||||
|
|
||||||
return updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a value based on an exact key path.
|
|
||||||
*
|
|
||||||
* @param keys The key hierarchy leading to the value.
|
|
||||||
* @return The value if found, otherwise null.
|
|
||||||
*/
|
|
||||||
public Object getValueByExactKeyPath(String... keys) {
|
|
||||||
return getValueByExactKeyPath(getRootNode(), new ArrayDeque<>(List.of(keys)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object getValueByExactKeyPath(Node node, Deque<String> keyQueue) {
|
|
||||||
if (!(node instanceof MappingNode mappingNode)) return null;
|
|
||||||
|
|
||||||
String currentKey = keyQueue.poll();
|
|
||||||
if (currentKey == null) return null;
|
|
||||||
|
|
||||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
|
||||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode
|
|
||||||
&& keyNode.getValue().equals(currentKey)) {
|
|
||||||
if (keyQueue.isEmpty()) {
|
|
||||||
Node valueNode = tuple.getValueNode();
|
|
||||||
|
|
||||||
if (valueNode instanceof ScalarNode scalarValueNode) {
|
|
||||||
return scalarValueNode.getValue();
|
|
||||||
} else if (valueNode instanceof MappingNode subMapping) {
|
|
||||||
return getValueByExactKeyPath(subMapping, keyQueue);
|
|
||||||
} else if (valueNode instanceof SequenceNode sequenceNode) {
|
|
||||||
List<Object> valuesList = new ArrayList<>();
|
|
||||||
for (Node o : sequenceNode.getValue()) {
|
|
||||||
if (o instanceof ScalarNode scalarValue) {
|
|
||||||
valuesList.add(scalarValue.getValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return valuesList;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getValueByExactKeyPath(tuple.getValueNode(), keyQueue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> cachedKeys;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the set of all keys present in the YAML structure. Keys are returned as
|
|
||||||
* dot-separated paths for nested keys.
|
|
||||||
*
|
|
||||||
* @return A set containing all keys in dot notation.
|
|
||||||
*/
|
|
||||||
public Set<String> getAllKeys() {
|
|
||||||
if (cachedKeys == null) {
|
|
||||||
cachedKeys = getAllKeys(getRootNode());
|
|
||||||
}
|
|
||||||
return cachedKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Collects all keys from the YAML node recursively.
|
|
||||||
*
|
|
||||||
* @param node The current YAML node.
|
|
||||||
* @param currentPath The accumulated path of keys.
|
|
||||||
* @param allKeys The set storing all collected keys.
|
|
||||||
*/
|
|
||||||
private Set<String> getAllKeys(Node node) {
|
|
||||||
Set<String> allKeys = new LinkedHashSet<>();
|
|
||||||
collectKeys(node, "", allKeys);
|
|
||||||
return allKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively traverses the YAML structure to collect all keys.
|
|
||||||
*
|
|
||||||
* @param node The current node in the YAML structure.
|
|
||||||
* @param currentPath The accumulated key path.
|
|
||||||
* @param allKeys The set storing collected keys.
|
|
||||||
*/
|
|
||||||
private void collectKeys(Node node, String currentPath, Set<String> allKeys) {
|
|
||||||
if (node instanceof MappingNode mappingNode) {
|
|
||||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
|
||||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode) {
|
|
||||||
String newPath =
|
|
||||||
currentPath.isEmpty()
|
|
||||||
? keyNode.getValue()
|
|
||||||
: currentPath + "." + keyNode.getValue();
|
|
||||||
allKeys.add(newPath);
|
|
||||||
collectKeys(tuple.getValueNode(), newPath, allKeys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the root node of the YAML document. If a new node was previously set, it is
|
|
||||||
* returned instead.
|
|
||||||
*
|
|
||||||
* @return The root node of the YAML structure.
|
|
||||||
*/
|
|
||||||
private Node getRootNode() {
|
|
||||||
if (this.updatedRootNode != null) {
|
|
||||||
return this.updatedRootNode;
|
|
||||||
}
|
|
||||||
Composer composer = new Composer(loadSettings, getParserImpl());
|
|
||||||
Optional<Node> rootNodeOpt = composer.getSingleNode();
|
|
||||||
if (rootNodeOpt.isPresent()) {
|
|
||||||
return rootNodeOpt.get();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a new root node, allowing modifications to be tracked.
|
|
||||||
*
|
|
||||||
* @param newRootNode The modified root node.
|
|
||||||
*/
|
|
||||||
public void setNewNode(Node newRootNode) {
|
|
||||||
this.updatedRootNode = newRootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the current root node (either the original or the updated one).
|
|
||||||
*
|
|
||||||
* @return The root node.
|
|
||||||
*/
|
|
||||||
public Node getUpdatedRootNode() {
|
|
||||||
if (this.updatedRootNode == null) {
|
|
||||||
this.updatedRootNode = getRootNode();
|
|
||||||
}
|
|
||||||
return this.updatedRootNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the YAML parser.
|
|
||||||
*
|
|
||||||
* @return The configured parser.
|
|
||||||
*/
|
|
||||||
private ParserImpl getParserImpl() {
|
|
||||||
return new ParserImpl(loadSettings, getStreamReader());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a stream reader for the YAML content.
|
|
||||||
*
|
|
||||||
* @return The configured stream reader.
|
|
||||||
*/
|
|
||||||
private StreamReader getStreamReader() {
|
|
||||||
return new StreamReader(loadSettings, yamlContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MappingNode save(Path saveFilePath) throws IOException {
|
|
||||||
if (!saveFilePath.equals(originalFilePath)) {
|
|
||||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
|
||||||
}
|
|
||||||
return (MappingNode) getUpdatedRootNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveOverride(Path saveFilePath) throws IOException {
|
|
||||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a YAML node back to a YAML-formatted string.
|
|
||||||
*
|
|
||||||
* @param rootNode The root node to be converted.
|
|
||||||
* @return A YAML-formatted string.
|
|
||||||
*/
|
|
||||||
public String convertNodeToYaml(Node rootNode) {
|
|
||||||
StringWriter writer = new StringWriter();
|
|
||||||
StreamDataWriter streamDataWriter =
|
|
||||||
new StreamDataWriter() {
|
|
||||||
@Override
|
|
||||||
public void write(String str) {
|
|
||||||
writer.write(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(String str, int off, int len) {
|
|
||||||
writer.write(str, off, len);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
new Dump(DUMP_SETTINGS).dumpNode(rootNode, streamDataWriter);
|
|
||||||
return writer.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isParsable(String value, Function<String, ?> parser) {
|
|
||||||
try {
|
|
||||||
parser.apply(value);
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given object is an integer.
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents an integer, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
|
||||||
public static boolean isInteger(Object object) {
|
|
||||||
if (object instanceof Integer
|
|
||||||
|| object instanceof Short
|
|
||||||
|| object instanceof Byte
|
|
||||||
|| object instanceof Long) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (object instanceof String str) {
|
|
||||||
return isParsable(str, Integer::parseInt);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given object is a floating-point number.
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents a float, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
|
||||||
public static boolean isFloat(Object object) {
|
|
||||||
return (object instanceof Float || object instanceof Double)
|
|
||||||
|| (object instanceof String str && isParsable(str, Float::parseFloat));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given object is a short integer.
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents a short integer, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
|
||||||
public static boolean isShort(Object object) {
|
|
||||||
return (object instanceof Long)
|
|
||||||
|| (object instanceof String str && isParsable(str, Short::parseShort));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given object is a byte.
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents a byte, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
|
||||||
public static boolean isByte(Object object) {
|
|
||||||
return (object instanceof Long)
|
|
||||||
|| (object instanceof String str && isParsable(str, Byte::parseByte));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a given object is a long integer.
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents a long integer, false otherwise.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
|
||||||
public static boolean isLong(Object object) {
|
|
||||||
return (object instanceof Long)
|
|
||||||
|| (object instanceof String str && isParsable(str, Long::parseLong));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines if an object is any type of integer (short, byte, long, or int).
|
|
||||||
*
|
|
||||||
* @param object The object to check.
|
|
||||||
* @return True if the object represents an integer type, false otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean isAnyInteger(Object object) {
|
|
||||||
return isInteger(object) || isShort(object) || isByte(object) || isLong(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies comments from an old node to a new one.
|
|
||||||
*
|
|
||||||
* @param oldNode The original node with comments.
|
|
||||||
* @param newValueNode The new node to which comments should be copied.
|
|
||||||
*/
|
|
||||||
private void copyComments(Node oldNode, Node newValueNode) {
|
|
||||||
if (oldNode == null || newValueNode == null) return;
|
|
||||||
if (oldNode.getBlockComments() != null) {
|
|
||||||
newValueNode.setBlockComments(oldNode.getBlockComments());
|
|
||||||
}
|
|
||||||
if (oldNode.getInLineComments() != null) {
|
|
||||||
newValueNode.setInLineComments(oldNode.getInLineComments());
|
|
||||||
}
|
|
||||||
if (oldNode.getEndComments() != null) {
|
|
||||||
newValueNode.setEndComments(oldNode.getEndComments());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package stirling.software.SPDF.config.interfaces;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
public interface DatabaseInterface {
|
public interface DatabaseInterface {
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -69,7 +67,7 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
}
|
}
|
||||||
if (exception instanceof BadCredentialsException
|
if (exception instanceof BadCredentialsException
|
||||||
|| exception instanceof UsernameNotFoundException) {
|
|| exception instanceof UsernameNotFoundException) {
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof InternalAuthenticationServiceException
|
if (exception instanceof InternalAuthenticationServiceException
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -14,75 +14,91 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
|||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import com.coveo.saml.SamlClient;
|
import com.coveo.saml.SamlClient;
|
||||||
import com.coveo.saml.SamlException;
|
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.SPDFApplication;
|
import stirling.software.SPDF.SPDFApplication;
|
||||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.Provider;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
public static final String LOGOUT_PATH = "/login?logout=true";
|
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
if (!response.isCommitted()) {
|
if (!response.isCommitted()) {
|
||||||
|
// Handle user logout due to disabled account
|
||||||
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle OAuth2 authentication error
|
||||||
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
// Handle SAML2 logout redirection
|
||||||
// Handle SAML2 logout redirection
|
if (authentication instanceof Saml2Authentication) {
|
||||||
getRedirect_saml2(request, response, samlAuthentication);
|
getRedirect_saml2(request, response, authentication);
|
||||||
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
return;
|
||||||
// Handle OAuth2 logout redirection
|
}
|
||||||
getRedirect_oauth2(request, response, oAuthToken);
|
// Handle OAuth2 logout redirection
|
||||||
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
else if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
// Handle Username/Password logout
|
getRedirect_oauth2(request, response, authentication);
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
return;
|
||||||
} else {
|
}
|
||||||
// Handle unknown authentication types
|
// Handle Username/Password logout
|
||||||
|
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Handle unknown authentication types
|
||||||
|
else {
|
||||||
log.error(
|
log.error(
|
||||||
"Authentication class unknown: {}",
|
"authentication class unknown: "
|
||||||
authentication.getClass().getSimpleName());
|
+ authentication.getClass().getSimpleName());
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login page after logout
|
// Redirect to login page after logout
|
||||||
String path = checkForErrors(request);
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
getRedirectStrategy().sendRedirect(request, response, path);
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect for SAML2 authentication logout
|
// Redirect for SAML2 authentication logout
|
||||||
private void getRedirect_saml2(
|
private void getRedirect_saml2(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
HttpServletResponse response,
|
|
||||||
Saml2Authentication samlAuthentication)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||||
String registrationId = samlConf.getRegistrationId();
|
String registrationId = samlConf.getRegistrationId();
|
||||||
|
|
||||||
|
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
CustomSaml2AuthenticatedPrincipal principal =
|
||||||
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
||||||
|
|
||||||
String nameIdValue = principal.name();
|
String nameIdValue = principal.getName();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read certificate from the resource
|
// Read certificate from the resource
|
||||||
@@ -93,7 +109,27 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
certificates.add(certificate);
|
certificates.add(certificate);
|
||||||
|
|
||||||
// Construct URLs required for SAML configuration
|
// Construct URLs required for SAML configuration
|
||||||
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
|
String serverUrl =
|
||||||
|
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||||
|
|
||||||
|
String relyingPartyIdentifier =
|
||||||
|
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||||
|
|
||||||
|
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||||
|
|
||||||
|
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
||||||
|
|
||||||
|
String idpIssuer = samlConf.getIdpIssuer();
|
||||||
|
|
||||||
|
// Create SamlClient instance for SAML logout
|
||||||
|
SamlClient samlClient =
|
||||||
|
new SamlClient(
|
||||||
|
relyingPartyIdentifier,
|
||||||
|
assertionConsumerServiceUrl,
|
||||||
|
idpUrl,
|
||||||
|
idpIssuer,
|
||||||
|
certificates,
|
||||||
|
SamlClient.SamlIdpBinding.POST);
|
||||||
|
|
||||||
// Read private key for service provider
|
// Read private key for service provider
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||||
@@ -105,134 +141,96 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
// Redirect to identity provider for logout
|
// Redirect to identity provider for logout
|
||||||
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(
|
log.error(nameIdValue, e);
|
||||||
"Error retrieving logout URL from Provider {} for user {}",
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
samlConf.getProvider(),
|
|
||||||
nameIdValue,
|
|
||||||
e);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect for OAuth2 authentication logout
|
// Redirect for OAuth2 authentication logout
|
||||||
private void getRedirect_oauth2(
|
private void getRedirect_oauth2(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
HttpServletResponse response,
|
|
||||||
OAuth2AuthenticationToken oAuthToken)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String registrationId;
|
String param = "logout=true";
|
||||||
|
String registrationId = null;
|
||||||
|
String issuer = null;
|
||||||
|
String clientId = null;
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
String path = checkForErrors(request);
|
|
||||||
|
|
||||||
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
|
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
||||||
|
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get OAuth2 provider details from configuration
|
||||||
|
Provider provider = oauth.getClient().get(registrationId);
|
||||||
|
issuer = provider.getIssuer();
|
||||||
|
clientId = provider.getClientId();
|
||||||
|
} catch (UnsupportedProviderException e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
|
issuer = oauth.getIssuer();
|
||||||
|
clientId = oauth.getClientId();
|
||||||
|
}
|
||||||
|
String errorMessage = "";
|
||||||
|
// Handle different error scenarios during logout
|
||||||
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
|
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||||
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
|
param = "error=" + sanitizeInput(errorMessage);
|
||||||
|
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
||||||
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
||||||
|
param = "erroroauth=oauth2_admin_blocked_user";
|
||||||
|
} else if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
param = "erroroauth=userIsDisabled";
|
||||||
|
} else if (request.getParameter("badcredentials") != null) {
|
||||||
|
param = "error=badcredentials";
|
||||||
|
}
|
||||||
|
|
||||||
|
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||||
|
|
||||||
// Redirect based on OAuth2 provider
|
// Redirect based on OAuth2 provider
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak" -> {
|
case "keycloak":
|
||||||
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
|
// Add Keycloak specific logout URL if needed
|
||||||
|
String logoutUrl =
|
||||||
boolean isKeycloak = !keycloak.getIssuer().isBlank();
|
issuer
|
||||||
boolean isCustomOAuth = !oauth.getIssuer().isBlank();
|
+ "/protocol/openid-connect/logout"
|
||||||
|
+ "?client_id="
|
||||||
String logoutUrl = redirectUrl;
|
+ clientId
|
||||||
|
+ "&post_logout_redirect_uri="
|
||||||
if (isKeycloak) {
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
logoutUrl = keycloak.getIssuer();
|
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
} else if (isCustomOAuth) {
|
|
||||||
logoutUrl = oauth.getIssuer();
|
|
||||||
}
|
|
||||||
if (isKeycloak || isCustomOAuth) {
|
|
||||||
logoutUrl +=
|
|
||||||
"/protocol/openid-connect/logout"
|
|
||||||
+ "?client_id="
|
|
||||||
+ oauth.getClientId()
|
|
||||||
+ "&post_logout_redirect_uri="
|
|
||||||
+ response.encodeRedirectURL(redirectUrl);
|
|
||||||
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
|
|
||||||
} else {
|
|
||||||
log.info(
|
|
||||||
"No redirect URL for {} available. Redirecting to default logout URL: {}",
|
|
||||||
registrationId,
|
|
||||||
logoutUrl);
|
|
||||||
}
|
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
}
|
break;
|
||||||
case "github", "google" -> {
|
case "github":
|
||||||
log.info(
|
// Add GitHub specific logout URL if needed
|
||||||
"No redirect URL for {} available. Redirecting to default logout URL: {}",
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
registrationId,
|
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
redirectUrl);
|
response.sendRedirect(githubLogoutUrl);
|
||||||
response.sendRedirect(redirectUrl);
|
break;
|
||||||
}
|
case "google":
|
||||||
default -> {
|
// Add Google specific logout URL if needed
|
||||||
log.info("Redirecting to default logout URL: {}", redirectUrl);
|
// String googleLogoutUrl =
|
||||||
response.sendRedirect(redirectUrl);
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
}
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
|
log.info("Google does not have a specific logout URL");
|
||||||
|
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
|
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||||
|
response.sendRedirect(defaultRedirectUrl);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SamlClient getSamlClient(
|
// Sanitize input to avoid potential security vulnerabilities
|
||||||
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
|
|
||||||
throws SamlException {
|
|
||||||
String serverUrl =
|
|
||||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
|
||||||
|
|
||||||
String relyingPartyIdentifier =
|
|
||||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
|
||||||
|
|
||||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
|
||||||
|
|
||||||
String idpSLOUrl = samlConf.getIdpSingleLogoutUrl();
|
|
||||||
|
|
||||||
String idpIssuer = samlConf.getIdpIssuer();
|
|
||||||
|
|
||||||
// Create SamlClient instance for SAML logout
|
|
||||||
return new SamlClient(
|
|
||||||
relyingPartyIdentifier,
|
|
||||||
assertionConsumerServiceUrl,
|
|
||||||
idpSLOUrl,
|
|
||||||
idpIssuer,
|
|
||||||
certificates,
|
|
||||||
SamlClient.SamlIdpBinding.POST);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
|
||||||
* the error request parameter.
|
|
||||||
*
|
|
||||||
* @param request the user's <code>HttpServletRequest</code> request.
|
|
||||||
* @return a <code>String</code> containing the error request parameter.
|
|
||||||
*/
|
|
||||||
private String checkForErrors(HttpServletRequest request) {
|
|
||||||
String errorMessage;
|
|
||||||
String path = "logout=true";
|
|
||||||
|
|
||||||
if (request.getParameter("oAuth2AuthenticationErrorWeb") != null) {
|
|
||||||
path = "errorOAuth=userAlreadyExistsWeb";
|
|
||||||
} else if ((errorMessage = request.getParameter("errorOAuth")) != null) {
|
|
||||||
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
|
||||||
} else if (request.getParameter("oAuth2AutoCreateDisabled") != null) {
|
|
||||||
path = "errorOAuth=oAuth2AutoCreateDisabled";
|
|
||||||
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
|
|
||||||
path = "errorOAuth=oAuth2AdminBlockedUser";
|
|
||||||
} else if (request.getParameter("userIsDisabled") != null) {
|
|
||||||
path = "errorOAuth=userIsDisabled";
|
|
||||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
|
||||||
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
|
||||||
} else if (request.getParameter("badCredentials") != null) {
|
|
||||||
path = "errorOAuth=badCredentials";
|
|
||||||
}
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize input to avoid potential security vulnerabilities. Will return a sanitised <code>
|
|
||||||
* String</code>.
|
|
||||||
*
|
|
||||||
* @return a sanitised <code>String</code>
|
|
||||||
*/
|
|
||||||
private String sanitizeInput(String input) {
|
private String sanitizeInput(String input) {
|
||||||
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
|
|
||||||
import jakarta.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
public class IPRateLimitingFilter implements Filter {
|
public class IPRateLimitingFilter implements Filter {
|
||||||
@@ -25,8 +24,8 @@ public class IPRateLimitingFilter implements Filter {
|
|||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
if (request instanceof HttpServletRequest httpServletRequest) {
|
if (request instanceof HttpServletRequest) {
|
||||||
HttpServletRequest httpRequest = httpServletRequest;
|
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||||
String method = httpRequest.getMethod();
|
String method = httpRequest.getMethod();
|
||||||
String requestURI = httpRequest.getRequestURI();
|
String requestURI = httpRequest.getRequestURI();
|
||||||
// Check if the request is for static resources
|
// Check if the request is for static resources
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import java.util.UUID;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -36,13 +34,12 @@ public class InitialSecuritySetup {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
try {
|
try {
|
||||||
|
if (databaseService.hasBackup()) {
|
||||||
|
databaseService.importDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
if (!userService.hasUsers()) {
|
if (!userService.hasUsers()) {
|
||||||
if (databaseService.hasBackup()) {
|
initializeAdminUser();
|
||||||
databaseService.importDatabase();
|
|
||||||
} else {
|
|
||||||
initializeAdminUser();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.migrateOauth2ToSSO();
|
userService.migrateOauth2ToSSO();
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.AttemptCounter;
|
import stirling.software.SPDF.model.AttemptCounter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.*;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -29,7 +29,6 @@ import org.springframework.security.web.savedrequest.NullRequestCache;
|
|||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
@@ -51,7 +50,11 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
private final CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Qualifier("loginEnabled")
|
||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Qualifier("runningEE")
|
||||||
private final boolean runningEE;
|
private final boolean runningEE;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
@@ -105,7 +108,6 @@ public class SecurityConfiguration {
|
|||||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -161,7 +163,8 @@ public class SecurityConfiguration {
|
|||||||
.logoutSuccessHandler(
|
.logoutSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(applicationProperties))
|
new CustomLogoutSuccessHandler(applicationProperties))
|
||||||
.clearAuthentication(true)
|
.clearAuthentication(true)
|
||||||
.invalidateHttpSession(true)
|
.invalidateHttpSession( // Invalidate session
|
||||||
|
true)
|
||||||
.deleteCookies("JSESSIONID", "remember-me"));
|
.deleteCookies("JSESSIONID", "remember-me"));
|
||||||
http.rememberMe(
|
http.rememberMe(
|
||||||
rememberMeConfigurer -> // Use the configurator directly
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
@@ -223,14 +226,14 @@ public class SecurityConfiguration {
|
|||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().isOauth2Active()) {
|
if (applicationProperties.getSecurity().isOauth2Activ()) {
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
oauth2.loginPage("/oauth2")
|
oauth2.loginPage("/oauth2")
|
||||||
.
|
.
|
||||||
/*
|
/*
|
||||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||||
is set as true, else login fails with an error message advising the same.
|
is set as true, else login fails with an error message advising the same.
|
||||||
*/
|
*/
|
||||||
successHandler(
|
successHandler(
|
||||||
@@ -254,7 +257,8 @@ public class SecurityConfiguration {
|
|||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
|
if (applicationProperties.getSecurity().isSaml2Activ()) {
|
||||||
|
// && runningEE
|
||||||
// Configure the authentication provider
|
// Configure the authentication provider
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
new OpenSaml4AuthenticationProvider();
|
new OpenSaml4AuthenticationProvider();
|
||||||
@@ -279,13 +283,12 @@ public class SecurityConfiguration {
|
|||||||
.authenticationRequestResolver(
|
.authenticationRequestResolver(
|
||||||
saml2AuthenticationRequestResolver);
|
saml2AuthenticationRequestResolver);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error configuring SAML 2 login", e);
|
log.error("Error configuring SAML2 login", e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info("SAML 2 login is not enabled. Using default.");
|
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
}
|
}
|
||||||
return http.build();
|
return http.build();
|
||||||
@@ -311,7 +314,7 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public boolean activeSecurity() {
|
public boolean activSecurity() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
@@ -88,7 +86,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
// provider for API keys.
|
// provider for API keys.
|
||||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||||
if (user.isEmpty()) {
|
if (!user.isPresent()) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
@@ -123,11 +121,9 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.write(
|
||||||
"Authentication required. Please provide a X-API-KEY in request"
|
"Authentication required. Please provide a X-API-KEY in request header.\n"
|
||||||
+ " header.\n"
|
|
||||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
+ "This is found in Settings -> Account Settings -> API Key\n"
|
||||||
+ "Alternatively you can disable authentication if this is"
|
+ "Alternatively you can disable authentication if this is unexpected");
|
||||||
+ " unexpected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,21 +139,21 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// Extract username and determine the login method
|
// Extract username and determine the login method
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = null;
|
String username = null;
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
if (principal instanceof UserDetails) {
|
||||||
username = detailsUser.getUsername();
|
username = ((UserDetails) principal).getUsername();
|
||||||
loginMethod = LoginMethod.USERDETAILS;
|
loginMethod = LoginMethod.USERDETAILS;
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
username = oAuth2User.getName();
|
username = ((OAuth2User) principal).getName();
|
||||||
loginMethod = LoginMethod.OAUTH2USER;
|
loginMethod = LoginMethod.OAUTH2USER;
|
||||||
OAUTH2 oAuth = securityProp.getOauth2();
|
OAUTH2 oAuth = securityProp.getOauth2();
|
||||||
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
username = saml2User.name();
|
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
loginMethod = LoginMethod.SAML2USER;
|
loginMethod = LoginMethod.SAML2USER;
|
||||||
SAML2 saml2 = securityProp.getSaml2();
|
SAML2 saml2 = securityProp.getSaml2();
|
||||||
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||||
} else if (principal instanceof String stringUser) {
|
} else if (principal instanceof String) {
|
||||||
username = stringUser;
|
username = (String) principal;
|
||||||
loginMethod = LoginMethod.STRINGUSER;
|
loginMethod = LoginMethod.STRINGUSER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,14 +168,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
boolean notSsoLogin =
|
boolean notSsoLogin =
|
||||||
!LoginMethod.OAUTH2USER.equals(loginMethod)
|
!loginMethod.equals(LoginMethod.OAUTH2USER)
|
||||||
&& !LoginMethod.SAML2USER.equals(loginMethod);
|
&& !loginMethod.equals(LoginMethod.SAML2USER);
|
||||||
|
|
||||||
// Block user registration if not allowed by configuration
|
// Block user registration if not allowed by configuration
|
||||||
if (blockRegistration && !isUserExists) {
|
if (blockRegistration && !isUserExists) {
|
||||||
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
request.getContextPath() + "/logout?oAuth2AdminBlockedUser=true");
|
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +191,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// Redirect to logout if credentials are invalid
|
// Redirect to logout if credentials are invalid
|
||||||
if (!isUserExists && notSsoLogin) {
|
if (!isUserExists && notSsoLogin) {
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?badCredentials=true");
|
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUserDisabled) {
|
if (isUserDisabled) {
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -21,13 +21,12 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.*;
|
import stirling.software.SPDF.model.*;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.repository.AuthorityRepository;
|
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@@ -78,18 +77,20 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public void processSSOPostLogin(String username, boolean autoCreateUser)
|
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
if (autoCreateUser) {
|
if (autoCreateUser) {
|
||||||
saveUser(username, AuthenticationType.SSO);
|
saveUser(username, AuthenticationType.SSO);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
@@ -121,14 +122,12 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
User user = saveUser(userOpt, generateApiKey());
|
if (user.isPresent()) {
|
||||||
try {
|
user.get().setApiKey(generateApiKey());
|
||||||
databaseService.exportDatabase();
|
return userRepository.save(user.get());
|
||||||
} catch (SQLException | UnsupportedProviderException e) {
|
|
||||||
log.error("Error exporting database after adding API key to user", e);
|
|
||||||
}
|
}
|
||||||
return user;
|
throw new UsernameNotFoundException("User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
@@ -140,9 +139,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
User user =
|
User user =
|
||||||
findByUsernameIgnoreCase(username)
|
findByUsernameIgnoreCase(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
if (user.getApiKey() == null || user.getApiKey().length() == 0) {
|
|
||||||
user = addApiKeyToUser(username);
|
|
||||||
}
|
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,14 +169,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private User saveUser(Optional<User> user, String apiKey) {
|
|
||||||
if (user.isPresent()) {
|
|
||||||
user.get().setApiKey(apiKey);
|
|
||||||
return userRepository.save(user.get());
|
|
||||||
}
|
|
||||||
throw new UsernameNotFoundException("User not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
@@ -381,18 +369,21 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public void invalidateUserSessions(String username) {
|
public void invalidateUserSessions(String username) {
|
||||||
String usernameP = "";
|
String usernameP = "";
|
||||||
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
for (SessionInformation sessionsInformation :
|
for (SessionInformation sessionsInformation :
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
if (principal instanceof UserDetails) {
|
||||||
usernameP = detailsUser.getUsername();
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
usernameP = userDetails.getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||||
usernameP = oAuth2User.getName();
|
usernameP = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
usernameP = saml2User.name();
|
CustomSaml2AuthenticatedPrincipal saml2User =
|
||||||
} else if (principal instanceof String stringUser) {
|
(CustomSaml2AuthenticatedPrincipal) principal;
|
||||||
usernameP = stringUser;
|
usernameP = saml2User.getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
usernameP = (String) principal;
|
||||||
}
|
}
|
||||||
if (usernameP.equalsIgnoreCase(username)) {
|
if (usernameP.equalsIgnoreCase(username)) {
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
@@ -403,56 +394,49 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public String getCurrentUsername() {
|
public String getCurrentUsername() {
|
||||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
return ((UserDetails) principal).getUsername();
|
||||||
return detailsUser.getUsername();
|
} else if (principal instanceof OAuth2User) {
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
return ((OAuth2User) principal)
|
||||||
return oAuth2User.getAttribute(
|
.getAttribute(
|
||||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
return saml2User.name();
|
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
} else if (principal instanceof String stringUser) {
|
} else if (principal instanceof String) {
|
||||||
return stringUser;
|
return (String) principal;
|
||||||
|
} else {
|
||||||
|
return principal.toString();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void syncCustomApiUser(String customApiKey) {
|
public void syncCustomApiUser(String customApiKey)
|
||||||
if (customApiKey == null || customApiKey.trim().isBlank()) {
|
throws SQLException, UnsupportedProviderException {
|
||||||
|
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = "CUSTOM_API_USER";
|
String username = "CUSTOM_API_USER";
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
|
if (!existingUser.isPresent()) {
|
||||||
existingUser.ifPresentOrElse(
|
// Create new user with API role
|
||||||
user -> {
|
User user = new User();
|
||||||
// Update API key if it has changed
|
user.setUsername(username);
|
||||||
User updatedUser = existingUser.get();
|
user.setPassword(UUID.randomUUID().toString());
|
||||||
|
user.setEnabled(true);
|
||||||
if (!customApiKey.equals(updatedUser.getApiKey())) {
|
user.setFirstLogin(false);
|
||||||
updatedUser.setApiKey(customApiKey);
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
userRepository.save(updatedUser);
|
user.setApiKey(customApiKey);
|
||||||
}
|
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||||
},
|
userRepository.save(user);
|
||||||
() -> {
|
|
||||||
// Create new user with API role
|
|
||||||
User user = new User();
|
|
||||||
user.setUsername(username);
|
|
||||||
user.setPassword(UUID.randomUUID().toString());
|
|
||||||
user.setEnabled(true);
|
|
||||||
user.setFirstLogin(false);
|
|
||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
|
||||||
user.setApiKey(customApiKey);
|
|
||||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
|
||||||
userRepository.save(user);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
databaseService.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
} catch (SQLException | UnsupportedProviderException e) {
|
} else {
|
||||||
log.error("Error exporting database after synchronising custom API user", e);
|
// Update API key if it has changed
|
||||||
|
User user = existingUser.get();
|
||||||
|
if (!customApiKey.equals(user.getApiKey())) {
|
||||||
|
user.setApiKey(customApiKey);
|
||||||
|
userRepository.save(user);
|
||||||
|
databaseService.exportDatabase();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.config.security.database;
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -9,10 +11,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@@ -35,8 +36,8 @@ public class DatabaseConfig {
|
|||||||
DATASOURCE_DEFAULT_URL =
|
DATASOURCE_DEFAULT_URL =
|
||||||
"jdbc:h2:file:"
|
"jdbc:h2:file:"
|
||||||
+ InstallationPathConfig.getConfigPath()
|
+ InstallationPathConfig.getConfigPath()
|
||||||
|
+ File.separator
|
||||||
+ "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
+ "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
||||||
log.debug("Database URL: {}", DATASOURCE_DEFAULT_URL);
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.runningEE = runningEE;
|
this.runningEE = runningEE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import org.springframework.jdbc.datasource.init.ScriptException;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Conditional(H2SQLCondition.class)
|
@Conditional(H2SQLCondition.class)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -29,7 +28,7 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
|
|
||||||
if (exception instanceof BadCredentialsException) {
|
if (exception instanceof BadCredentialsException) {
|
||||||
log.error("BadCredentialsException", exception);
|
log.error("BadCredentialsException", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof DisabledException) {
|
if (exception instanceof DisabledException) {
|
||||||
@@ -42,20 +41,18 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof OAuth2AuthenticationException oAuth2Exception) {
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
OAuth2Error error = oAuth2Exception.getError();
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
String errorCode = error.getErrorCode();
|
String errorCode = error.getErrorCode();
|
||||||
|
|
||||||
if ("Password must not be null".equals(error.getErrorCode())) {
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
errorCode = "userAlreadyExistsWeb";
|
errorCode = "userAlreadyExistsWeb";
|
||||||
}
|
}
|
||||||
|
log.error("OAuth2 Authentication error: " + errorCode);
|
||||||
log.error(
|
log.error("OAuth2AuthenticationException", exception);
|
||||||
"OAuth2 Authentication error: {}",
|
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
|
||||||
errorCode != null ? errorCode : exception.getMessage(),
|
return;
|
||||||
exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
|
|
||||||
}
|
}
|
||||||
log.error("Unhandled authentication exception", exception);
|
log.error("Unhandled authentication exception", exception);
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
|||||||
@@ -14,24 +14,24 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
public class CustomOAuth2AuthenticationSuccessHandler
|
public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private final LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
private final UserService userService;
|
private ApplicationProperties applicationProperties;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
public CustomOAuth2AuthenticationSuccessHandler(
|
public CustomOAuth2AuthenticationSuccessHandler(
|
||||||
LoginAttemptService loginAttemptService,
|
final LoginAttemptService loginAttemptService,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
UserService userService) {
|
UserService userService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
@@ -47,10 +47,12 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = "";
|
String username = "";
|
||||||
|
|
||||||
if (principal instanceof OAuth2User oAuth2User) {
|
if (principal instanceof OAuth2User) {
|
||||||
username = oAuth2User.getName();
|
OAuth2User oauthUser = (OAuth2User) principal;
|
||||||
} else if (principal instanceof UserDetails detailsUser) {
|
username = oauthUser.getName();
|
||||||
username = detailsUser.getUsername();
|
} else if (principal instanceof UserDetails) {
|
||||||
|
UserDetails oauthUser = (UserDetails) principal;
|
||||||
|
username = oauthUser.getUsername();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
@@ -75,7 +77,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userService.isUserDisabled(username)) {
|
if (userService.isUserDisabled(username)) {
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
@@ -85,14 +86,13 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (oAuth.getBlockRegistration()
|
if (oAuth.getBlockRegistration()
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User) {
|
||||||
|
|||||||
@@ -12,24 +12,23 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
|||||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.UsernameAttribute;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
|
|
||||||
private final OidcUserService delegate = new OidcUserService();
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
|
||||||
private final UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
private final LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public CustomOAuth2UserService(
|
public CustomOAuth2UserService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@@ -42,26 +41,34 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||||
|
String usernameAttribute = oauth2.getUseAsUsername();
|
||||||
|
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
||||||
|
Client client = oauth2.getClient();
|
||||||
|
if (client != null && client.getKeycloak() != null) {
|
||||||
|
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
||||||
|
} else {
|
||||||
|
usernameAttribute = "email";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OidcUser user = delegate.loadUser(userRequest);
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
||||||
UsernameAttribute usernameAttribute =
|
|
||||||
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
|
||||||
String usernameAttributeKey = usernameAttribute.getName();
|
|
||||||
|
|
||||||
// todo: save user by OIDC ID instead of username
|
// Check if the username claim is null or empty
|
||||||
Optional<User> internalUser =
|
if (username == null || username.trim().isEmpty()) {
|
||||||
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
|
throw new IllegalArgumentException(
|
||||||
|
"Claim '" + usernameAttribute + "' cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
if (internalUser.isPresent()) {
|
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||||
String internalUsername = internalUser.get().getUsername();
|
if (duser.isPresent()) {
|
||||||
if (loginAttemptService.isBlocked(internalUsername)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"The account "
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
+ internalUsername
|
|
||||||
+ " has been locked due to too many failed login attempts.");
|
|
||||||
}
|
}
|
||||||
if (userService.hasPassword(usernameAttributeKey)) {
|
if (userService.hasPassword(username)) {
|
||||||
throw new IllegalArgumentException("Password must not be null");
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +78,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
user.getAuthorities(),
|
user.getAuthorities(),
|
||||||
userRequest.getIdToken(),
|
userRequest.getIdToken(),
|
||||||
user.getUserInfo(),
|
user.getUserInfo(),
|
||||||
usernameAttributeKey);
|
usernameAttribute);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.error("Error loading OIDC user: {}", e.getMessage());
|
log.error("Error loading OIDC user: {}", e.getMessage());
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package stirling.software.SPDF.config.security.oauth2;
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
|
|
||||||
import static stirling.software.SPDF.utils.validation.Validator.*;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,26 +20,23 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.UsernameAttribute;
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
import stirling.software.SPDF.model.exception.NoProviderFoundException;
|
|
||||||
import stirling.software.SPDF.model.provider.GitHubProvider;
|
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.model.provider.Provider;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
@Slf4j
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public class OAuth2Configuration {
|
public class OAuth2Configuration {
|
||||||
|
|
||||||
public static final String REDIRECT_URI_PATH = "{baseUrl}/login/oauth2/code/";
|
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
@Lazy private final UserService userService;
|
@Lazy private final UserService userService;
|
||||||
|
|
||||||
@@ -53,175 +47,139 @@ public class OAuth2Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
public ClientRegistrationRepository clientRegistrationRepository()
|
value = "security.oauth2.enabled",
|
||||||
throws NoProviderFoundException {
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
List<ClientRegistration> registrations = new ArrayList<>();
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||||||
githubClientRegistration().ifPresent(registrations::add);
|
githubClientRegistration().ifPresent(registrations::add);
|
||||||
oidcClientRegistration().ifPresent(registrations::add);
|
oidcClientRegistration().ifPresent(registrations::add);
|
||||||
googleClientRegistration().ifPresent(registrations::add);
|
googleClientRegistration().ifPresent(registrations::add);
|
||||||
keycloakClientRegistration().ifPresent(registrations::add);
|
keycloakClientRegistration().ifPresent(registrations::add);
|
||||||
|
|
||||||
if (registrations.isEmpty()) {
|
if (registrations.isEmpty()) {
|
||||||
log.error("No OAuth2 provider registered");
|
log.error("At least one OAuth2 provider must be configured");
|
||||||
throw new NoProviderFoundException("At least one OAuth2 provider must be configured.");
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InMemoryClientRegistrationRepository(registrations);
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> keycloakClientRegistration() {
|
private Optional<ClientRegistration> googleClientRegistration() {
|
||||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
if (isOAuth2Enabled(oauth2) || isClientInitialised(oauth2)) {
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
return google != null && google.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistration.withRegistrationId(google.getName())
|
||||||
|
.clientId(google.getClientId())
|
||||||
|
.clientSecret(google.getClientSecret())
|
||||||
|
.scope(google.getScopes())
|
||||||
|
.authorizationUri(google.getAuthorizationuri())
|
||||||
|
.tokenUri(google.getTokenuri())
|
||||||
|
.userInfoUri(google.getUserinfouri())
|
||||||
|
.userNameAttributeName(google.getUseAsUsername())
|
||||||
|
.clientName(google.getClientName())
|
||||||
|
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
||||||
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
Client client = oauth2.getClient();
|
private Optional<ClientRegistration> keycloakClientRegistration() {
|
||||||
KeycloakProvider keycloakClient = client.getKeycloak();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
Provider keycloak =
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
new KeycloakProvider(
|
return Optional.empty();
|
||||||
keycloakClient.getIssuer(),
|
}
|
||||||
keycloakClient.getClientId(),
|
Client client = oauth.getClient();
|
||||||
keycloakClient.getClientSecret(),
|
if (client == null) {
|
||||||
keycloakClient.getScopes(),
|
return Optional.empty();
|
||||||
keycloakClient.getUseAsUsername());
|
}
|
||||||
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
return validateProvider(keycloak)
|
return keycloak != null && keycloak.isSettingsValid()
|
||||||
? Optional.of(
|
? Optional.of(
|
||||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||||
.registrationId(keycloak.getName())
|
.registrationId(keycloak.getName())
|
||||||
.clientId(keycloak.getClientId())
|
.clientId(keycloak.getClientId())
|
||||||
.clientSecret(keycloak.getClientSecret())
|
.clientSecret(keycloak.getClientSecret())
|
||||||
.scope(keycloak.getScopes())
|
.scope(keycloak.getScopes())
|
||||||
.userNameAttributeName(keycloak.getUseAsUsername().getName())
|
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||||
.clientName(keycloak.getClientName())
|
.clientName(keycloak.getClientName())
|
||||||
.build())
|
.build())
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> googleClientRegistration() {
|
|
||||||
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
|
||||||
|
|
||||||
if (isOAuth2Enabled(oAuth2) || isClientInitialised(oAuth2)) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
Client client = oAuth2.getClient();
|
|
||||||
GoogleProvider googleClient = client.getGoogle();
|
|
||||||
Provider google =
|
|
||||||
new GoogleProvider(
|
|
||||||
googleClient.getClientId(),
|
|
||||||
googleClient.getClientSecret(),
|
|
||||||
googleClient.getScopes(),
|
|
||||||
googleClient.getUseAsUsername());
|
|
||||||
|
|
||||||
return validateProvider(google)
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistration.withRegistrationId(google.getName())
|
|
||||||
.clientId(google.getClientId())
|
|
||||||
.clientSecret(google.getClientSecret())
|
|
||||||
.scope(google.getScopes())
|
|
||||||
.authorizationUri(google.getAuthorizationUri())
|
|
||||||
.tokenUri(google.getTokenUri())
|
|
||||||
.userInfoUri(google.getUserInfoUri())
|
|
||||||
.userNameAttributeName(google.getUseAsUsername().getName())
|
|
||||||
.clientName(google.getClientName())
|
|
||||||
.redirectUri(REDIRECT_URI_PATH + google.getName())
|
|
||||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
if (isOAuth2Enabled(oAuth2)) {
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
Client client = oAuth2.getClient();
|
if (client == null) {
|
||||||
GitHubProvider githubClient = client.getGithub();
|
return Optional.empty();
|
||||||
Provider github =
|
}
|
||||||
new GitHubProvider(
|
GithubProvider github = client.getGithub();
|
||||||
githubClient.getClientId(),
|
return github != null && github.isSettingsValid()
|
||||||
githubClient.getClientSecret(),
|
|
||||||
githubClient.getScopes(),
|
|
||||||
githubClient.getUseAsUsername());
|
|
||||||
|
|
||||||
return validateProvider(github)
|
|
||||||
? Optional.of(
|
? Optional.of(
|
||||||
ClientRegistration.withRegistrationId(github.getName())
|
ClientRegistration.withRegistrationId(github.getName())
|
||||||
.clientId(github.getClientId())
|
.clientId(github.getClientId())
|
||||||
.clientSecret(github.getClientSecret())
|
.clientSecret(github.getClientSecret())
|
||||||
.scope(github.getScopes())
|
.scope(github.getScopes())
|
||||||
.authorizationUri(github.getAuthorizationUri())
|
.authorizationUri(github.getAuthorizationuri())
|
||||||
.tokenUri(github.getTokenUri())
|
.tokenUri(github.getTokenuri())
|
||||||
.userInfoUri(github.getUserInfoUri())
|
.userInfoUri(github.getUserinfouri())
|
||||||
.userNameAttributeName(github.getUseAsUsername().getName())
|
.userNameAttributeName(github.getUseAsUsername())
|
||||||
.clientName(github.getClientName())
|
.clientName(github.getClientName())
|
||||||
.redirectUri(REDIRECT_URI_PATH + github.getName())
|
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
||||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
.build())
|
.build())
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> oidcClientRegistration() {
|
private Optional<ClientRegistration> oidcClientRegistration() {
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
if (oauth == null
|
||||||
if (isOAuth2Enabled(oauth) || isClientInitialised(oauth)) {
|
|| oauth.getIssuer() == null
|
||||||
|
|| oauth.getIssuer().isEmpty()
|
||||||
|
|| oauth.getClientId() == null
|
||||||
|
|| oauth.getClientId().isEmpty()
|
||||||
|
|| oauth.getClientSecret() == null
|
||||||
|
|| oauth.getClientSecret().isEmpty()
|
||||||
|
|| oauth.getScopes() == null
|
||||||
|
|| oauth.getScopes().isEmpty()
|
||||||
|
|| oauth.getUseAsUsername() == null
|
||||||
|
|| oauth.getUseAsUsername().isEmpty()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
return Optional.of(
|
||||||
String name = oauth.getProvider();
|
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||||
String firstChar = String.valueOf(name.charAt(0));
|
.registrationId("oidc")
|
||||||
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
|
.clientId(oauth.getClientId())
|
||||||
|
.clientSecret(oauth.getClientSecret())
|
||||||
Provider oidcProvider =
|
.scope(oauth.getScopes())
|
||||||
new Provider(
|
.userNameAttributeName(oauth.getUseAsUsername())
|
||||||
oauth.getIssuer(),
|
.clientName("OIDC")
|
||||||
name,
|
.build());
|
||||||
clientName,
|
|
||||||
oauth.getClientId(),
|
|
||||||
oauth.getClientSecret(),
|
|
||||||
oauth.getScopes(),
|
|
||||||
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
|
|
||||||
oauth.getLogoutUrl(),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
|
|
||||||
return !isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider)
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
|
||||||
.registrationId(name)
|
|
||||||
.clientId(oidcProvider.getClientId())
|
|
||||||
.clientSecret(oidcProvider.getClientSecret())
|
|
||||||
.scope(oidcProvider.getScopes())
|
|
||||||
.userNameAttributeName(oidcProvider.getUseAsUsername().getName())
|
|
||||||
.clientName(clientName)
|
|
||||||
.redirectUri(REDIRECT_URI_PATH + name)
|
|
||||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isOAuth2Enabled(OAUTH2 oAuth2) {
|
|
||||||
return oAuth2 == null || !oAuth2.getEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isClientInitialised(OAUTH2 oauth2) {
|
|
||||||
Client client = oauth2.getClient();
|
|
||||||
return client == null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
return (authorities) -> {
|
return (authorities) -> {
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
@@ -230,7 +188,7 @@ public class OAuth2Configuration {
|
|||||||
// Add existing OAUTH2 Authorities
|
// Add existing OAUTH2 Authorities
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
// Add Authorities from database for existing user, if user is present.
|
// Add Authorities from database for existing user, if user is present.
|
||||||
if (authority instanceof OAuth2UserAuthority oAuth2Auth) {
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
||||||
String useAsUsername =
|
String useAsUsername =
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getSecurity()
|
.getSecurity()
|
||||||
@@ -238,12 +196,14 @@ public class OAuth2Configuration {
|
|||||||
.getUseAsUsername();
|
.getUseAsUsername();
|
||||||
Optional<User> userOpt =
|
Optional<User> userOpt =
|
||||||
userService.findByUsernameIgnoreCase(
|
userService.findByUsernameIgnoreCase(
|
||||||
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
mappedAuthorities.add(
|
if (user != null) {
|
||||||
new SimpleGrantedAuthority(
|
mappedAuthorities.add(
|
||||||
userService.findRole(user).getAuthority()));
|
new SimpleGrantedAuthority(
|
||||||
|
userService.findRole(user).getAuthority()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,10 +13,8 @@ import org.bouncycastle.openssl.PEMParser;
|
|||||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||||
import org.bouncycastle.util.io.pem.PemObject;
|
import org.bouncycastle.util.io.pem.PemObject;
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
import org.bouncycastle.util.io.pem.PemReader;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
|
||||||
public class CertificateUtils {
|
public class CertificateUtils {
|
||||||
|
|
||||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||||
@@ -40,12 +38,13 @@ public class CertificateUtils {
|
|||||||
Object object = pemParser.readObject();
|
Object object = pemParser.readObject();
|
||||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||||
|
|
||||||
if (object instanceof PEMKeyPair keypair) {
|
if (object instanceof PEMKeyPair) {
|
||||||
// Handle traditional RSA private key format
|
// Handle traditional RSA private key format
|
||||||
|
PEMKeyPair keypair = (PEMKeyPair) object;
|
||||||
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
||||||
} else if (object instanceof PrivateKeyInfo keyInfo) {
|
} else if (object instanceof PrivateKeyInfo) {
|
||||||
// Handle PKCS#8 format
|
// Handle PKCS#8 format
|
||||||
return (RSAPrivateKey) converter.getPrivateKey(keyInfo);
|
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unsupported key format: "
|
"Unsupported key format: "
|
||||||
|
|||||||
@@ -4,17 +4,27 @@ import java.io.Serializable;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||||
|
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
public class CustomSaml2AuthenticatedPrincipal
|
||||||
public record CustomSaml2AuthenticatedPrincipal(
|
|
||||||
String name,
|
|
||||||
Map<String, List<Object>> attributes,
|
|
||||||
String nameId,
|
|
||||||
List<String> sessionIndexes)
|
|
||||||
implements Saml2AuthenticatedPrincipal, Serializable {
|
implements Saml2AuthenticatedPrincipal, Serializable {
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final Map<String, List<Object>> attributes;
|
||||||
|
private final String nameId;
|
||||||
|
private final List<String> sessionIndexes;
|
||||||
|
|
||||||
|
public CustomSaml2AuthenticatedPrincipal(
|
||||||
|
String name,
|
||||||
|
Map<String, List<Object>> attributes,
|
||||||
|
String nameId,
|
||||||
|
List<String> sessionIndexes) {
|
||||||
|
this.name = name;
|
||||||
|
this.attributes = attributes;
|
||||||
|
this.nameId = nameId;
|
||||||
|
this.sessionIndexes = sessionIndexes;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
@@ -24,4 +34,12 @@ public record CustomSaml2AuthenticatedPrincipal(
|
|||||||
public Map<String, List<Object>> getAttributes() {
|
public Map<String, List<Object>> getAttributes() {
|
||||||
return this.attributes;
|
return this.attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getNameId() {
|
||||||
|
return this.nameId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getSessionIndexes() {
|
||||||
|
return this.sessionIndexes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,18 @@ package stirling.software.SPDF.config.security.saml2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.saml2.core.Saml2Error;
|
import org.springframework.security.saml2.core.Saml2Error;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
|
||||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -23,19 +21,18 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
|||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException {
|
throws IOException, ServletException {
|
||||||
log.error("Authentication error", exception);
|
|
||||||
|
|
||||||
if (exception instanceof Saml2AuthenticationException) {
|
if (exception instanceof Saml2AuthenticationException) {
|
||||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode());
|
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||||
} else if (exception instanceof ProviderNotFoundException) {
|
} else if (exception instanceof ProviderNotFoundException) {
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(
|
.sendRedirect(
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
"/login?errorOAuth=not_authentication_provider_found");
|
"/login?erroroauth=not_authentication_provider_found");
|
||||||
}
|
}
|
||||||
|
log.error("AuthenticationException: " + exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,14 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -41,8 +39,8 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
log.debug("Starting SAML2 authentication success handling");
|
log.debug("Starting SAML2 authentication success handling");
|
||||||
|
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2Principal) {
|
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
String username = saml2Principal.name();
|
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
log.debug("Authenticated principal found for user: {}", username);
|
log.debug("Authenticated principal found for user: {}", username);
|
||||||
|
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
@@ -97,7 +95,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,18 +103,20 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
if (saml2.getBlockRegistration() && !userExists) {
|
if (saml2.getBlockRegistration() && !userExists) {
|
||||||
log.debug("Registration blocked for new user: {}", username);
|
log.debug("Registration blocked for new user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug("Processing SSO post-login for user: {}", username);
|
log.debug("Processing SSO post-login for user: {}", username);
|
||||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||||
log.debug("Successfully processed authentication for user: {}", username);
|
log.debug("Successfully processed authentication for user: {}", username);
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
|
return;
|
||||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Invalid username detected for user: {}, redirecting to logout",
|
"Invalid username detected for user: {}, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,23 +7,20 @@ import org.opensaml.saml.saml2.core.Assertion;
|
|||||||
import org.opensaml.saml.saml2.core.Attribute;
|
import org.opensaml.saml.saml2.core.Attribute;
|
||||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||||
import org.opensaml.saml.saml2.core.AuthnStatement;
|
import org.opensaml.saml.saml2.core.AuthnStatement;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
import org.springframework.core.convert.converter.Converter;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
|
||||||
public class CustomSaml2ResponseAuthenticationConverter
|
public class CustomSaml2ResponseAuthenticationConverter
|
||||||
implements Converter<ResponseToken, Saml2Authentication> {
|
implements Converter<ResponseToken, Saml2Authentication> {
|
||||||
|
|
||||||
private final UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
@@ -63,10 +60,10 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||||
|
|
||||||
// Debug log with actual values
|
// Debug log with actual values
|
||||||
log.debug("Extracted SAML Attributes: {}", attributes);
|
log.debug("Extracted SAML Attributes: " + attributes);
|
||||||
|
|
||||||
// Try to get username/identifier in order of preference
|
// Try to get username/identifier in order of preference
|
||||||
String userIdentifier;
|
String userIdentifier = null;
|
||||||
if (hasAttribute(attributes, "username")) {
|
if (hasAttribute(attributes, "username")) {
|
||||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||||
@@ -86,8 +83,10 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
simpleGrantedAuthority =
|
if (user != null) {
|
||||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
simpleGrantedAuthority =
|
||||||
|
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> sessionIndexes = new ArrayList<>();
|
List<String> sessionIndexes = new ArrayList<>();
|
||||||
@@ -102,7 +101,7 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
return new Saml2Authentication(
|
return new Saml2Authentication(
|
||||||
principal,
|
principal,
|
||||||
responseToken.getToken().getSaml2Response(),
|
responseToken.getToken().getSaml2Response(),
|
||||||
List.of(simpleGrantedAuthority));
|
Collections.singletonList(simpleGrantedAuthority));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||||
|
|||||||
@@ -11,37 +11,40 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
|
value = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public class SAML2Configuration {
|
public class SAML2Configuration {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
||||||
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
|
name = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
|
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||||
Resource certificateResource = samlConf.getSpCert();
|
Resource certificateResource = samlConf.getSpCert();
|
||||||
@@ -53,124 +56,81 @@ public class SAML2Configuration {
|
|||||||
RelyingPartyRegistration rp =
|
RelyingPartyRegistration rp =
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||||
.signingX509Credentials(c -> c.add(signingCredential))
|
.signingX509Credentials(c -> c.add(signingCredential))
|
||||||
.entityId(samlConf.getIdpIssuer())
|
|
||||||
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
|
||||||
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
|
||||||
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
|
|
||||||
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
|
||||||
.assertionConsumerServiceLocation(
|
|
||||||
"{baseUrl}/login/saml2/sso/{registrationId}")
|
|
||||||
.assertingPartyMetadata(
|
.assertingPartyMetadata(
|
||||||
metadata ->
|
metadata ->
|
||||||
metadata.entityId(samlConf.getIdpIssuer())
|
metadata.entityId(samlConf.getIdpIssuer())
|
||||||
|
.singleSignOnServiceLocation(
|
||||||
|
samlConf.getIdpSingleLoginUrl())
|
||||||
.verificationX509Credentials(
|
.verificationX509Credentials(
|
||||||
c -> c.add(verificationCredential))
|
c -> c.add(verificationCredential))
|
||||||
.singleSignOnServiceBinding(
|
.singleSignOnServiceBinding(
|
||||||
Saml2MessageBinding.POST)
|
Saml2MessageBinding.POST)
|
||||||
.singleSignOnServiceLocation(
|
|
||||||
samlConf.getIdpSingleLoginUrl())
|
|
||||||
.singleLogoutServiceBinding(
|
|
||||||
Saml2MessageBinding.POST)
|
|
||||||
.singleLogoutServiceLocation(
|
|
||||||
samlConf.getIdpSingleLogoutUrl())
|
|
||||||
.wantAuthnRequestsSigned(true))
|
.wantAuthnRequestsSigned(true))
|
||||||
.build();
|
.build();
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
@ConditionalOnProperty(
|
||||||
|
name = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||||
OpenSaml4AuthenticationRequestResolver resolver =
|
OpenSaml4AuthenticationRequestResolver resolver =
|
||||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||||
|
|
||||||
resolver.setAuthnRequestCustomizer(
|
resolver.setAuthnRequestCustomizer(
|
||||||
customizer -> {
|
customizer -> {
|
||||||
HttpServletRequest request = customizer.getRequest();
|
log.debug("Customizing SAML Authentication request");
|
||||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||||
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
|
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||||
new HttpSessionSaml2AuthenticationRequestRepository();
|
if (authnRequest.getID() == null) {
|
||||||
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||||
requestRepository.loadAuthenticationRequest(request);
|
}
|
||||||
|
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||||
if (saml2AuthenticationRequest != null) {
|
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||||
String sessionId = request.getSession(false).getId();
|
log.debug(
|
||||||
|
"AuthnRequest Issuer: {}",
|
||||||
log.debug(
|
authnRequest.getIssuer() != null
|
||||||
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
|
? authnRequest.getIssuer().getValue()
|
||||||
sessionId);
|
: "null");
|
||||||
|
HttpServletRequest request = customizer.getRequest();
|
||||||
String authenticationRequestId = saml2AuthenticationRequest.getId();
|
// Log HTTP request details
|
||||||
|
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||||
if (!authenticationRequestId.isBlank()) {
|
log.debug("Request URI: {}", request.getRequestURI());
|
||||||
authnRequest.setID(authenticationRequestId);
|
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||||
} else {
|
log.debug("Query String: {}", request.getQueryString());
|
||||||
log.warn(
|
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||||
"No authentication request found for HTTP session {}. Generating new ID",
|
// Log headers
|
||||||
sessionId);
|
Collections.list(request.getHeaderNames())
|
||||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
.forEach(
|
||||||
}
|
headerName -> {
|
||||||
} else {
|
log.debug(
|
||||||
log.debug("Generating new authentication request ID");
|
"Header - {}: {}",
|
||||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
headerName,
|
||||||
|
request.getHeader(headerName));
|
||||||
|
});
|
||||||
|
// Log SAML specific parameters
|
||||||
|
log.debug("SAML Request Parameters:");
|
||||||
|
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||||
|
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||||
|
// Log session debugrmation if exists
|
||||||
|
if (request.getSession(false) != null) {
|
||||||
|
log.debug("Session ID: {}", request.getSession().getId());
|
||||||
|
}
|
||||||
|
// Log any assertions consumer service details if present
|
||||||
|
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||||
|
log.debug(
|
||||||
|
"AssertionConsumerServiceURL: {}",
|
||||||
|
authnRequest.getAssertionConsumerServiceURL());
|
||||||
|
}
|
||||||
|
// Log NameID policy if present
|
||||||
|
if (authnRequest.getNameIDPolicy() != null) {
|
||||||
|
log.debug(
|
||||||
|
"NameIDPolicy Format: {}",
|
||||||
|
authnRequest.getNameIDPolicy().getFormat());
|
||||||
}
|
}
|
||||||
|
|
||||||
logAuthnRequestDetails(authnRequest);
|
|
||||||
logHttpRequestDetails(request);
|
|
||||||
});
|
});
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
|
|
||||||
String message =
|
|
||||||
"""
|
|
||||||
AuthnRequest:
|
|
||||||
|
|
||||||
ID: {}
|
|
||||||
Issuer: {}
|
|
||||||
IssueInstant: {}
|
|
||||||
AssertionConsumerService (ACS) URL: {}
|
|
||||||
""";
|
|
||||||
log.debug(
|
|
||||||
message,
|
|
||||||
authnRequest.getID(),
|
|
||||||
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
|
|
||||||
authnRequest.getIssueInstant(),
|
|
||||||
authnRequest.getAssertionConsumerServiceURL());
|
|
||||||
|
|
||||||
if (authnRequest.getNameIDPolicy() != null) {
|
|
||||||
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void logHttpRequestDetails(HttpServletRequest request) {
|
|
||||||
log.debug("HTTP Headers: ");
|
|
||||||
Collections.list(request.getHeaderNames())
|
|
||||||
.forEach(
|
|
||||||
headerName ->
|
|
||||||
log.debug("{}: {}", headerName, request.getHeader(headerName)));
|
|
||||||
String message =
|
|
||||||
"""
|
|
||||||
HTTP Request Method: {}
|
|
||||||
Session ID: {}
|
|
||||||
Request Path: {}
|
|
||||||
Query String: {}
|
|
||||||
Remote Address: {}
|
|
||||||
|
|
||||||
SAML Request Parameters:
|
|
||||||
|
|
||||||
SAMLRequest: {}
|
|
||||||
RelayState: {}
|
|
||||||
""";
|
|
||||||
log.debug(
|
|
||||||
message,
|
|
||||||
request.getMethod(),
|
|
||||||
request.getSession().getId(),
|
|
||||||
request.getRequestURI(),
|
|
||||||
request.getQueryString(),
|
|
||||||
request.getRemoteAddr(),
|
|
||||||
request.getParameter("SAMLRequest"),
|
|
||||||
request.getParameter("RelayState"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@@ -43,14 +42,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||||
String principalName = null;
|
String principalName = null;
|
||||||
|
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
if (principal instanceof UserDetails) {
|
||||||
principalName = detailsUser.getUsername();
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
principalName = oAuth2User.getName();
|
principalName = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
principalName = saml2User.name();
|
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
} else if (principal instanceof String stringUser) {
|
} else if (principal instanceof String) {
|
||||||
principalName = stringUser;
|
principalName = (String) principal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
@@ -74,14 +73,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
public void registerNewSession(String sessionId, Object principal) {
|
public void registerNewSession(String sessionId, Object principal) {
|
||||||
String principalName = null;
|
String principalName = null;
|
||||||
|
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
if (principal instanceof UserDetails) {
|
||||||
principalName = detailsUser.getUsername();
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
principalName = oAuth2User.getName();
|
principalName = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
principalName = saml2User.name();
|
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
} else if (principal instanceof String stringUser) {
|
} else if (principal instanceof String) {
|
||||||
principalName = stringUser;
|
principalName = (String) principal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import org.springframework.data.repository.query.Param;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import stirling.software.SPDF.service.LanguageService;
|
import stirling.software.SPDF.service.LanguageService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -33,10 +32,7 @@ public class AdditionalLanguageJsController {
|
|||||||
response.setContentType("application/javascript");
|
response.setContentType("application/javascript");
|
||||||
PrintWriter writer = response.getWriter();
|
PrintWriter writer = response.getWriter();
|
||||||
// Erstelle das JavaScript dynamisch
|
// Erstelle das JavaScript dynamisch
|
||||||
writer.println(
|
writer.println("const supportedLanguages = " + toJsonArray(new ArrayList<>(supportedLanguages)) + ";");
|
||||||
"const supportedLanguages = "
|
|
||||||
+ toJsonArray(new ArrayList<>(supportedLanguages))
|
|
||||||
+ ";");
|
|
||||||
// Generiere die `getDetailedLanguageCode`-Funktion
|
// Generiere die `getDetailedLanguageCode`-Funktion
|
||||||
writer.println(
|
writer.println(
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import io.swagger.v3.oas.annotations.Parameter;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.database.DatabaseService;
|
import stirling.software.SPDF.config.security.database.DatabaseService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SortTypes;
|
import stirling.software.SPDF.model.SortTypes;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
@@ -175,38 +174,7 @@ public class RearrangePagesPDFController {
|
|||||||
return newPageOrderZeroBased;
|
return newPageOrderZeroBased;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Integer> duplicate(int totalPages, String pageOrder) {
|
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
|
||||||
List<Integer> newPageOrder = new ArrayList<>();
|
|
||||||
int duplicateCount;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Parse the duplicate count from pageOrder
|
|
||||||
duplicateCount =
|
|
||||||
pageOrder != null && !pageOrder.isEmpty()
|
|
||||||
? Integer.parseInt(pageOrder.trim())
|
|
||||||
: 2; // Default to 2 if not specified
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.error("Invalid duplicate count specified", e);
|
|
||||||
duplicateCount = 2; // Default to 2 if invalid input
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate duplicate count
|
|
||||||
if (duplicateCount < 1) {
|
|
||||||
duplicateCount = 2; // Default to 2 if invalid input
|
|
||||||
}
|
|
||||||
|
|
||||||
// For each page in the document
|
|
||||||
for (int pageNum = 0; pageNum < totalPages; pageNum++) {
|
|
||||||
// Add the current page index duplicateCount times
|
|
||||||
for (int dupCount = 0; dupCount < duplicateCount; dupCount++) {
|
|
||||||
newPageOrder.add(pageNum);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newPageOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Integer> processSortTypes(String sortTypes, int totalPages, String pageOrder) {
|
|
||||||
try {
|
try {
|
||||||
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
@@ -228,8 +196,6 @@ public class RearrangePagesPDFController {
|
|||||||
return removeLast(totalPages);
|
return removeLast(totalPages);
|
||||||
case REMOVE_FIRST_AND_LAST:
|
case REMOVE_FIRST_AND_LAST:
|
||||||
return removeFirstAndLast(totalPages);
|
return removeFirstAndLast(totalPages);
|
||||||
case DUPLICATE:
|
|
||||||
return duplicate(totalPages, pageOrder);
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException("Unsupported custom mode");
|
throw new IllegalArgumentException("Unsupported custom mode");
|
||||||
}
|
}
|
||||||
@@ -257,10 +223,8 @@ public class RearrangePagesPDFController {
|
|||||||
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
|
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
|
||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
List<Integer> newPageOrder;
|
List<Integer> newPageOrder;
|
||||||
if (sortType != null
|
if (sortType != null && sortType.length() > 0) {
|
||||||
&& sortType.length() > 0
|
newPageOrder = processSortTypes(sortType, totalPages);
|
||||||
&& !"custom".equals(sortType.toLowerCase())) {
|
|
||||||
newPageOrder = processSortTypes(sortType, totalPages, pageOrder);
|
|
||||||
} else {
|
} else {
|
||||||
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
|
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ public class SettingsController {
|
|||||||
@PostMapping("/update-enable-analytics")
|
@PostMapping("/update-enable-analytics")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||||
.body(
|
.body(
|
||||||
"Setting has already been set, To adjust please edit "
|
"Setting has already been set, To adjust please edit "
|
||||||
+ InstallationPathConfig.getSettingsPath());
|
+ InstallationPathConfig.getSettingsPath());
|
||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||||
applicationProperties.getSystem().setEnableAnalytics(enabled);
|
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||||
return ResponseEntity.ok("Updated");
|
return ResponseEntity.ok("Updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PdfMetadata;
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||||
import stirling.software.SPDF.service.PdfMetadataService;
|
import stirling.software.SPDF.service.PdfMetadataService;
|
||||||
|
|||||||
@@ -100,8 +100,6 @@ public class SplitPdfBySectionsController {
|
|||||||
|
|
||||||
if (sectionNum == horiz * verti) pageNum++;
|
if (sectionNum == horiz * verti) pageNum++;
|
||||||
}
|
}
|
||||||
|
|
||||||
zipOut.finish();
|
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
@@ -36,7 +34,7 @@ import stirling.software.SPDF.model.AuthenticationType;
|
|||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "User", description = "User APIs")
|
@Tag(name = "User", description = "User APIs")
|
||||||
@@ -126,7 +124,7 @@ public class UserController {
|
|||||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -154,7 +152,7 @@ public class UserController {
|
|||||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
if (userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound", true);
|
return new RedirectView("/account?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -176,7 +174,7 @@ public class UserController {
|
|||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
}
|
}
|
||||||
log.debug("Processed updates: {}", updates);
|
log.debug("Processed updates: " + updates);
|
||||||
// Assuming you have a method in userService to update the settings for a user
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
userService.updateUserSettings(principal.getName(), updates);
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
// Redirect to a page of your choice after updating
|
// Redirect to a page of your choice after updating
|
||||||
@@ -199,7 +197,7 @@ public class UserController {
|
|||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user.getUsername().equalsIgnoreCase(username)) {
|
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +274,7 @@ public class UserController {
|
|||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws SQLException, UnsupportedProviderException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isEmpty()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
@@ -295,20 +293,20 @@ public class UserController {
|
|||||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
String userNameP = "";
|
String userNameP = "";
|
||||||
for (Object principal : principals) {
|
for (Object principal : principals) {
|
||||||
List<SessionInformation> sessionsInformation =
|
List<SessionInformation> sessionsInformations =
|
||||||
sessionRegistry.getAllSessions(principal, false);
|
sessionRegistry.getAllSessions(principal, false);
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
if (principal instanceof UserDetails) {
|
||||||
userNameP = detailsUser.getUsername();
|
userNameP = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
userNameP = oAuth2User.getName();
|
userNameP = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
userNameP = saml2User.name();
|
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
} else if (principal instanceof String stringUser) {
|
} else if (principal instanceof String) {
|
||||||
userNameP = stringUser;
|
userNameP = (String) principal;
|
||||||
}
|
}
|
||||||
if (userNameP.equalsIgnoreCase(username)) {
|
if (userNameP.equalsIgnoreCase(username)) {
|
||||||
for (SessionInformation sessionInfo : sessionsInformation) {
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
// @RestController
|
||||||
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
// @RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertBookToPDFController {
|
||||||
|
|
||||||
|
private final boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ConvertBookToPDFController(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
|
||||||
|
@Operation(
|
||||||
|
summary =
|
||||||
|
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
|
||||||
|
description =
|
||||||
|
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
|
||||||
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInput == null) {
|
||||||
|
throw new IllegalArgumentException("Please provide a file for conversion.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
||||||
|
|
||||||
|
if (originalFilename != null) {
|
||||||
|
String originalFilenameLower = originalFilename.toLowerCase();
|
||||||
|
if (!originalFilenameLower.endsWith(".epub")
|
||||||
|
&& !originalFilenameLower.endsWith(".mobi")
|
||||||
|
&& !originalFilenameLower.endsWith(".azw3")
|
||||||
|
&& !originalFilenameLower.endsWith(".fb2")
|
||||||
|
&& !originalFilenameLower.endsWith(".txt")
|
||||||
|
&& !originalFilenameLower.endsWith(".docx")) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"File must be in .epub, .mobi, .azw3, .fb2, .txt, or .docx format.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
|
||||||
|
|
||||||
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
|
|
||||||
|
String outputFilename =
|
||||||
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -12,9 +13,8 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.RuntimePathConfig;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -24,28 +24,27 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertHtmlToPDF {
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
|
private final boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertHtmlToPDF(
|
public ConvertHtmlToPDF(
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
ApplicationProperties applicationProperties,
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled,
|
||||||
RuntimePathConfig runtimePathConfig) {
|
ApplicationProperties applicationProperties) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.runtimePathConfig = runtimePathConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||||
description =
|
description =
|
||||||
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format. Input:HTML Output:PDF Type:SISO")
|
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format.")
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
@@ -61,16 +60,15 @@ public class ConvertHtmlToPDF {
|
|||||||
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
throw new IllegalArgumentException("File must be either .html or .zip format.");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean disableSanitize =
|
boolean disableSanitize = Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
runtimePathConfig.getWeasyPrintPath(),
|
|
||||||
request,
|
request,
|
||||||
fileInput.getBytes(),
|
fileInput.getBytes(),
|
||||||
originalFilename,
|
originalFilename,
|
||||||
disableSanitize);
|
bookAndHtmlFormatsInstalled,
|
||||||
|
disableSanitize);
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.commonmark.parser.Parser;
|
|||||||
import org.commonmark.renderer.html.AttributeProvider;
|
import org.commonmark.renderer.html.AttributeProvider;
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -22,9 +23,8 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.RuntimePathConfig;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -34,20 +34,20 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertMarkdownToPdf {
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
|
private final boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertMarkdownToPdf(
|
public ConvertMarkdownToPdf(
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
ApplicationProperties applicationProperties,
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled,
|
||||||
RuntimePathConfig runtimePathConfig) {
|
ApplicationProperties applicationProperties) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.runtimePathConfig = runtimePathConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@@ -81,16 +81,15 @@ public class ConvertMarkdownToPdf {
|
|||||||
|
|
||||||
String htmlContent = renderer.render(document);
|
String htmlContent = renderer.render(document);
|
||||||
|
|
||||||
boolean disableSanitize =
|
boolean disableSanitize = Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
||||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
|
||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
runtimePathConfig.getWeasyPrintPath(),
|
|
||||||
null,
|
null,
|
||||||
htmlContent.getBytes(),
|
htmlContent.getBytes(),
|
||||||
"converted.html",
|
"converted.html",
|
||||||
disableSanitize);
|
bookAndHtmlFormatsInstalled,
|
||||||
|
disableSanitize);
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.RuntimePathConfig;
|
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
@@ -35,13 +34,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertOfficeController {
|
public class ConvertOfficeController {
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertOfficeController(
|
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
this.runtimePathConfig = runtimePathConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||||
@@ -65,13 +61,13 @@ public class ConvertOfficeController {
|
|||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
runtimePathConfig.getUnoConvertPath(),
|
"unoconv",
|
||||||
"--port",
|
"-vvv",
|
||||||
"2003",
|
"-f",
|
||||||
"--convert-to",
|
|
||||||
"pdf",
|
"pdf",
|
||||||
tempInputFile.toString(),
|
"-o",
|
||||||
tempOutputFile.toString()));
|
tempOutputFile.toString(),
|
||||||
|
tempInputFile.toString()));
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.converters.PdfToBookRequest;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
// @RestController
|
||||||
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
// @RequestMapping("/api/v1/convert")
|
||||||
|
public class ConvertPDFToBookController {
|
||||||
|
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
|
private final boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
|
public ConvertPDFToBookController(
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
||||||
|
@Operation(
|
||||||
|
summary =
|
||||||
|
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
|
||||||
|
description =
|
||||||
|
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
|
||||||
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
||||||
|
}
|
||||||
|
if (fileInput == null) {
|
||||||
|
throw new IllegalArgumentException("Please provide a file for conversion.");
|
||||||
|
}
|
||||||
|
// Validate the output format
|
||||||
|
String outputFormat = request.getOutputFormat().toLowerCase();
|
||||||
|
List<String> allowedFormats =
|
||||||
|
Arrays.asList(
|
||||||
|
"epub", "mobi", "azw3", "docx", "rtf", "txt", "html", "lit", "fb2", "pdb",
|
||||||
|
"lrf");
|
||||||
|
if (!allowedFormats.contains(outputFormat)) {
|
||||||
|
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
|
||||||
|
}
|
||||||
|
byte[] outputFileBytes;
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
Path tempOutputFile =
|
||||||
|
Files.createTempFile(
|
||||||
|
"output_", // Use the output format for the file extension
|
||||||
|
"." + outputFormat);
|
||||||
|
Path tempInputFile = null;
|
||||||
|
try {
|
||||||
|
// Create temp input file from the provided PDF
|
||||||
|
// Assuming input is always PDF
|
||||||
|
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
|
Files.write(tempInputFile, fileInput.getBytes());
|
||||||
|
command.add("ebook-convert");
|
||||||
|
command.add(tempInputFile.toString());
|
||||||
|
command.add(tempOutputFile.toString());
|
||||||
|
ProcessExecutorResult returnCode =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
outputFileBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
} finally {
|
||||||
|
// Clean up temporary files
|
||||||
|
if (tempInputFile != null) {
|
||||||
|
Files.deleteIfExists(tempInputFile);
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
}
|
||||||
|
String outputFilename =
|
||||||
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "."
|
||||||
|
+ // Remove file extension and append .pdf
|
||||||
|
outputFormat;
|
||||||
|
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
@@ -74,8 +73,8 @@ public class ConvertPDFToPDFA {
|
|||||||
// Determine PDF/A filter based on requested format
|
// Determine PDF/A filter based on requested format
|
||||||
String pdfFilter =
|
String pdfFilter =
|
||||||
"pdfa".equals(outputFormat)
|
"pdfa".equals(outputFormat)
|
||||||
? "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"2\"}}"
|
? "writer_pdf_Export:{'SelectPdfVersion':{'Value':'2'}}:writer_pdf_Export"
|
||||||
: "pdf:writer_pdf_Export:{\"SelectPdfVersion\":{\"type\":\"long\",\"value\":\"1\"}}";
|
: "writer_pdf_Export:{'SelectPdfVersion':{'Value':'1'}}:writer_pdf_Export";
|
||||||
|
|
||||||
// Prepare LibreOffice command
|
// Prepare LibreOffice command
|
||||||
List<String> command =
|
List<String> command =
|
||||||
@@ -85,7 +84,7 @@ public class ConvertPDFToPDFA {
|
|||||||
"--headless",
|
"--headless",
|
||||||
"--nologo",
|
"--nologo",
|
||||||
"--convert-to",
|
"--convert-to",
|
||||||
pdfFilter,
|
"pdf:" + pdfFilter,
|
||||||
"--outdir",
|
"--outdir",
|
||||||
tempOutputDir.toString(),
|
tempOutputDir.toString(),
|
||||||
tempInputFile.toString()));
|
tempInputFile.toString()));
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.RuntimePathConfig;
|
|
||||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
@@ -34,13 +32,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertWebsiteToPDF {
|
public class ConvertWebsiteToPDF {
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
private final RuntimePathConfig runtimePathConfig;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertWebsiteToPDF(
|
public ConvertWebsiteToPDF(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
this.runtimePathConfig = runtimePathConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
@@ -70,7 +65,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
|
|
||||||
// Prepare the WeasyPrint command
|
// Prepare the WeasyPrint command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add(runtimePathConfig.getWeasyPrintPath());
|
command.add("weasyprint");
|
||||||
command.add(URL);
|
command.add(URL);
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
import org.apache.commons.csv.CSVFormat;
|
import org.apache.commons.csv.CSVFormat;
|
||||||
import org.apache.commons.csv.QuoteMode;
|
import org.apache.commons.csv.QuoteMode;
|
||||||
@@ -26,20 +19,17 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
|
||||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||||
|
|
||||||
import technology.tabula.ObjectExtractor;
|
import technology.tabula.ObjectExtractor;
|
||||||
import technology.tabula.Page;
|
import technology.tabula.Page;
|
||||||
import technology.tabula.Table;
|
import technology.tabula.Table;
|
||||||
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||||
|
import technology.tabula.writers.Writer;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
@Slf4j
|
|
||||||
public class ExtractCSVController {
|
public class ExtractCSVController {
|
||||||
|
|
||||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||||
@@ -47,83 +37,31 @@ public class ExtractCSVController {
|
|||||||
summary = "Extracts a CSV document from a PDF",
|
summary = "Extracts a CSV document from a PDF",
|
||||||
description =
|
description =
|
||||||
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||||
public ResponseEntity<?> pdfToCsv(@ModelAttribute PDFWithPageNums form) throws Exception {
|
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
|
||||||
String baseName = getBaseName(form.getFileInput().getOriginalFilename());
|
StringWriter writer = new StringWriter();
|
||||||
List<CsvEntry> csvEntries = new ArrayList<>();
|
|
||||||
|
|
||||||
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||||
List<Integer> pages = form.getPageNumbersList(document, true);
|
|
||||||
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
|
||||||
CSVFormat format =
|
CSVFormat format =
|
||||||
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
||||||
|
Writer csvWriter = new FlexibleCSVWriter(format);
|
||||||
for (int pageNum : pages) {
|
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
||||||
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
||||||
log.info("{}", pageNum);
|
Page page = extractor.extract(form.getPageId());
|
||||||
Page page = extractor.extract(pageNum);
|
List<Table> tables = sea.extract(page);
|
||||||
List<Table> tables = sea.extract(page);
|
csvWriter.write(writer, tables);
|
||||||
|
|
||||||
for (int i = 0; i < tables.size(); i++) {
|
|
||||||
StringWriter sw = new StringWriter();
|
|
||||||
FlexibleCSVWriter csvWriter = new FlexibleCSVWriter(format);
|
|
||||||
csvWriter.write(sw, Collections.singletonList(tables.get(i)));
|
|
||||||
|
|
||||||
String entryName = generateEntryName(baseName, pageNum, i + 1);
|
|
||||||
csvEntries.add(new CsvEntry(entryName, sw.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (csvEntries.isEmpty()) {
|
|
||||||
return ResponseEntity.noContent().build();
|
|
||||||
} else if (csvEntries.size() == 1) {
|
|
||||||
return createCsvResponse(csvEntries.get(0), baseName);
|
|
||||||
} else {
|
|
||||||
return createZipResponse(csvEntries, baseName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ResponseEntity<byte[]> createZipResponse(List<CsvEntry> entries, String baseName)
|
|
||||||
throws IOException {
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
|
|
||||||
for (CsvEntry entry : entries) {
|
|
||||||
ZipEntry zipEntry = new ZipEntry(entry.filename());
|
|
||||||
zipOut.putNextEntry(zipEntry);
|
|
||||||
zipOut.write(entry.content().getBytes(StandardCharsets.UTF_8));
|
|
||||||
zipOut.closeEntry();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentDisposition(
|
headers.setContentDisposition(
|
||||||
ContentDisposition.builder("attachment")
|
ContentDisposition.builder("attachment")
|
||||||
.filename(baseName + "_extracted.zip")
|
.filename(
|
||||||
.build());
|
form.getFileInput()
|
||||||
headers.setContentType(MediaType.parseMediaType("application/zip"));
|
.getOriginalFilename()
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
return ResponseEntity.ok().headers(headers).body(baos.toByteArray());
|
+ "_extracted.csv")
|
||||||
}
|
|
||||||
|
|
||||||
private ResponseEntity<String> createCsvResponse(CsvEntry entry, String baseName) {
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
|
||||||
headers.setContentDisposition(
|
|
||||||
ContentDisposition.builder("attachment")
|
|
||||||
.filename(baseName + "_extracted.csv")
|
|
||||||
.build());
|
.build());
|
||||||
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(entry.content());
|
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String generateEntryName(String baseName, int pageNum, int tableIndex) {
|
|
||||||
return String.format("%s_p%d_t%d.csv", baseName, pageNum, tableIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getBaseName(String filename) {
|
|
||||||
return filename.replaceFirst("[.][^.]+$", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
private record CsvEntry(String filename, String content) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user