Compare commits
155 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958c214605 | ||
|
|
56ab9c3eee | ||
|
|
7b768db969 | ||
|
|
36c4d8552d | ||
|
|
4544fb7211 | ||
|
|
807639308a | ||
|
|
49fb634690 | ||
|
|
715efca25d | ||
|
|
a4c988c7b2 | ||
|
|
f45de05c99 | ||
|
|
d5faddbc85 | ||
|
|
5e173b92d4 | ||
|
|
574c474804 | ||
|
|
2e23149090 | ||
|
|
7be8db7832 | ||
|
|
702433d4c5 | ||
|
|
0e824005c3 | ||
|
|
8c2a468915 | ||
|
|
7d81ca799f | ||
|
|
0f24fa104e | ||
|
|
db1fa28230 | ||
|
|
9884c65b10 | ||
|
|
2abfe599ed | ||
|
|
cbdec6ec25 | ||
|
|
d4f25db647 | ||
|
|
0f1b94303f | ||
|
|
4dfdf2d8ec | ||
|
|
d6937b1784 | ||
|
|
3ae732352e | ||
|
|
f9b90692fb | ||
|
|
04f72f151d | ||
|
|
73ee9651a3 | ||
|
|
a0f3d7b7d4 | ||
|
|
7223b379e7 | ||
|
|
e833d688e7 | ||
|
|
74cab98a1b | ||
|
|
5c1abdd49e | ||
|
|
341a234b7c | ||
|
|
34ddc298cf | ||
|
|
f4ae4ac5c4 | ||
|
|
91aece2284 | ||
|
|
2a1dad80f0 | ||
|
|
f1bf42544d | ||
|
|
2fd8d8809b | ||
|
|
b2628d4c3f | ||
|
|
50fb665c29 | ||
|
|
b0881cdb4c | ||
|
|
988a877199 | ||
|
|
80f53e972a | ||
|
|
6697cccfa9 | ||
|
|
9820a3276d | ||
|
|
fb6cefa301 | ||
|
|
974b947fc3 | ||
|
|
4e45bae856 | ||
|
|
74c6ea7c60 | ||
|
|
15abef1b82 | ||
|
|
fb61fea3e4 | ||
|
|
f29d4db31e | ||
|
|
b86eac20ea | ||
|
|
7ccb9db9f9 | ||
|
|
e920eb0555 | ||
|
|
822ccbabdf | ||
|
|
0d2b5f605f | ||
|
|
76924ccd2f | ||
|
|
12e8aceb4c | ||
|
|
46261c55b5 | ||
|
|
d2e3c2a12c | ||
|
|
58bcb9fdb1 | ||
|
|
ae44c4c723 | ||
|
|
a7f900b959 | ||
|
|
c0873050a3 | ||
|
|
f09e70218d | ||
|
|
e0d37d2e10 | ||
|
|
70cc03fc63 | ||
|
|
4ee756f7dd | ||
|
|
67a08a7c05 | ||
|
|
d136f9bd31 | ||
|
|
a909592533 | ||
|
|
6a609712ba | ||
|
|
238db1aaff | ||
|
|
c08329ec48 | ||
|
|
1defa441d6 | ||
|
|
51577a0a69 | ||
|
|
0366bc6bf9 | ||
|
|
b6c66c47cd | ||
|
|
de6bfa2af2 | ||
|
|
2dd580cf57 | ||
|
|
5df445eabf | ||
|
|
d3e0c0bce1 | ||
|
|
5c0a606d66 | ||
|
|
226cbe1a8e | ||
|
|
f13945b05f | ||
|
|
889b612cf3 | ||
|
|
9aec9d3451 | ||
|
|
b65ac4032e | ||
|
|
e7f2576857 | ||
|
|
c877f122c4 | ||
|
|
ff99c46434 | ||
|
|
8f5c5211d1 | ||
|
|
125bebf9ff | ||
|
|
dd03875ece | ||
|
|
6456a28436 | ||
|
|
1bfa534dcd | ||
|
|
7b78c5cbf1 | ||
|
|
adb715f2f2 | ||
|
|
b3230f1e52 | ||
|
|
e5d62042c4 | ||
|
|
801a2a792b | ||
|
|
b8466c2b97 | ||
|
|
1006aa110e | ||
|
|
daae6bfd3e | ||
|
|
1795d5764a | ||
|
|
9dcb3009f0 | ||
|
|
ca9abf76be | ||
|
|
3870f73949 | ||
|
|
dc5b214932 | ||
|
|
bba3d65368 | ||
|
|
e3d2bd3b1b | ||
|
|
a71cb7f5d9 | ||
|
|
a9e250547a | ||
|
|
30bcdbd7be | ||
|
|
61603aebb2 | ||
|
|
26340626df | ||
|
|
8dca4a588d | ||
|
|
63386baa0d | ||
|
|
2a93910da3 | ||
|
|
fca6dc1fd8 | ||
|
|
2d82c5fa77 | ||
|
|
167c792bf0 | ||
|
|
95a9e10dc8 | ||
|
|
4d6368048c | ||
|
|
4e715a82e0 | ||
|
|
164381e940 | ||
|
|
0436f45de5 | ||
|
|
6ce761aff2 | ||
|
|
8a5d9f9a95 | ||
|
|
c0ef624a1d | ||
|
|
b9ae90274f | ||
|
|
764b8f4d22 | ||
|
|
a531f53893 | ||
|
|
74d6d96f4e | ||
|
|
1862ab1671 | ||
|
|
bf95ca43dc | ||
|
|
678ef85da1 | ||
|
|
9a6afdd921 | ||
|
|
a72615cc86 | ||
|
|
b0daac2566 | ||
|
|
2335ecf017 | ||
|
|
a79318bf0c | ||
|
|
9eed761346 | ||
|
|
12d86049f6 | ||
|
|
e9f80d03ea | ||
|
|
42c238d87d | ||
|
|
051cd2d0d5 | ||
|
|
faf3454a02 |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1,2 +1,2 @@
|
|||||||
# All PRs to V1 must be approved by Frooodle
|
# All PRs to V1 must be approved by Frooodle
|
||||||
* @Frooodle
|
* @Frooodle @reecebrowne @Ludy87 @DarioGii
|
||||||
|
|||||||
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -11,7 +11,13 @@ updates:
|
|||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
rebase-strategy: "auto"
|
rebase-strategy: "auto"
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/" # Location of Dockerfile
|
directory: "/" # Location of Dockerfile
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
|||||||
6
.github/labeler-config.yml
vendored
6
.github/labeler-config.yml
vendored
@@ -30,6 +30,8 @@ Security:
|
|||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
|
||||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||||
|
|
||||||
API:
|
API:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
@@ -37,6 +39,7 @@ API:
|
|||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
||||||
- any-glob-to-any-file: 'split_photos.py'
|
- any-glob-to-any-file: 'split_photos.py'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/swagger.yml'
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
@@ -57,6 +60,9 @@ Test:
|
|||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'cucumber/**/*'
|
- any-glob-to-any-file: 'cucumber/**/*'
|
||||||
- any-glob-to-any-file: 'src/test**/*'
|
- any-glob-to-any-file: 'src/test**/*'
|
||||||
|
- any-glob-to-any-file: '.pre-commit-config'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/pre_commit.yml'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||||
|
|
||||||
Github:
|
Github:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
4
.github/release.yml
vendored
4
.github/release.yml
vendored
@@ -9,7 +9,7 @@ changelog:
|
|||||||
- title: Bug Fixes
|
- title: Bug Fixes
|
||||||
labels:
|
labels:
|
||||||
- Bug
|
- Bug
|
||||||
|
|
||||||
- title: Enhancements
|
- title: Enhancements
|
||||||
labels:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
@@ -26,7 +26,7 @@ changelog:
|
|||||||
- title: Translation Changes
|
- title: Translation Changes
|
||||||
labels:
|
labels:
|
||||||
- Translation
|
- Translation
|
||||||
|
|
||||||
- title: Other Changes
|
- title: Other Changes
|
||||||
labels:
|
labels:
|
||||||
- "*"
|
- "*"
|
||||||
|
|||||||
48
.github/workflows/PR-Demo-Comment.yml
vendored
48
.github/workflows/PR-Demo-Comment.yml
vendored
@@ -8,14 +8,14 @@ jobs:
|
|||||||
check-comment:
|
check-comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: |
|
||||||
github.event.issue.pull_request &&
|
github.event.issue.pull_request &&
|
||||||
(
|
(
|
||||||
contains(github.event.comment.body, 'prdeploy') ||
|
contains(github.event.comment.body, 'prdeploy') ||
|
||||||
contains(github.event.comment.body, 'deploypr')
|
contains(github.event.comment.body, 'deploypr')
|
||||||
)
|
)
|
||||||
&&
|
&&
|
||||||
(
|
(
|
||||||
github.event.comment.user.login == 'frooodle' ||
|
github.event.comment.user.login == 'frooodle' ||
|
||||||
github.event.comment.user.login == 'sf298' ||
|
github.event.comment.user.login == 'sf298' ||
|
||||||
github.event.comment.user.login == 'Ludy87' ||
|
github.event.comment.user.login == 'Ludy87' ||
|
||||||
github.event.comment.user.login == 'LaserKaspar' ||
|
github.event.comment.user.login == 'LaserKaspar' ||
|
||||||
@@ -28,9 +28,14 @@ jobs:
|
|||||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Get PR data
|
- name: Get PR data
|
||||||
id: get-pr
|
id: get-pr
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const prNumber = context.payload.issue.number;
|
const prNumber = context.payload.issue.number;
|
||||||
@@ -39,41 +44,46 @@ jobs:
|
|||||||
|
|
||||||
- name: Get PR repository and ref
|
- name: Get PR repository and ref
|
||||||
id: get-pr-info
|
id: get-pr-info
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { owner, repo } = context.repo;
|
const { owner, repo } = context.repo;
|
||||||
const prNumber = context.payload.issue.number;
|
const prNumber = context.payload.issue.number;
|
||||||
|
|
||||||
const { data: pr } = await github.rest.pulls.get({
|
const { data: pr } = await github.rest.pulls.get({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
pull_number: prNumber,
|
pull_number: prNumber,
|
||||||
});
|
});
|
||||||
|
|
||||||
// For forks, use the full repository name, for internal PRs use the current repo
|
// For forks, use the full repository name, for internal PRs use the current repo
|
||||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||||
|
|
||||||
console.log(`PR Repository: ${repository}`);
|
console.log(`PR Repository: ${repository}`);
|
||||||
console.log(`PR Branch: ${pr.head.ref}`);
|
console.log(`PR Branch: ${pr.head.ref}`);
|
||||||
|
|
||||||
core.setOutput('repository', repository);
|
core.setOutput('repository', repository);
|
||||||
core.setOutput('ref', pr.head.ref);
|
core.setOutput('ref', pr.head.ref);
|
||||||
|
|
||||||
deploy-pr:
|
deploy-pr:
|
||||||
needs: check-comment
|
needs: check-comment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout PR
|
- name: Checkout PR
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
@@ -84,20 +94,20 @@ jobs:
|
|||||||
DOCKER_ENABLE_SECURITY: false
|
DOCKER_ENABLE_SECURITY: false
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
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@v6
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -146,10 +156,10 @@ jobs:
|
|||||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||||
# Create PR-specific directories
|
# Create PR-specific directories
|
||||||
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
||||||
|
|
||||||
# Move docker-compose file to correct location
|
# Move docker-compose file to correct location
|
||||||
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
||||||
|
|
||||||
# Start or restart the container
|
# Start or restart the container
|
||||||
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
@@ -158,7 +168,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Post deployment URL to PR
|
- name: Post deployment URL to PR
|
||||||
if: success()
|
if: success()
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { GITHUB_REPOSITORY } = process.env;
|
const { GITHUB_REPOSITORY } = process.env;
|
||||||
|
|||||||
28
.github/workflows/PR-Demo-cleanup.yml
vendored
28
.github/workflows/PR-Demo-cleanup.yml
vendored
@@ -4,9 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, closed]
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
permissions:
|
permissions: read-all
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
||||||
@@ -15,9 +13,17 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
if: github.event.action == 'closed'
|
if: github.event.action == 'closed'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Set up SSH
|
- name: Set up SSH
|
||||||
run: |
|
run: |
|
||||||
mkdir -p ~/.ssh/
|
mkdir -p ~/.ssh/
|
||||||
@@ -30,20 +36,20 @@ jobs:
|
|||||||
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||||
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||||
echo "Found PR directory, proceeding with cleanup..."
|
echo "Found PR directory, proceeding with cleanup..."
|
||||||
|
|
||||||
# Stop and remove containers
|
# Stop and remove containers
|
||||||
cd /stirling/PR-${{ github.event.pull_request.number }}
|
cd /stirling/PR-${{ github.event.pull_request.number }}
|
||||||
docker-compose down || true
|
docker-compose down || true
|
||||||
|
|
||||||
# Go back to root before removal
|
# Go back to root before removal
|
||||||
cd /
|
cd /
|
||||||
|
|
||||||
# Remove PR-specific directories
|
# Remove PR-specific directories
|
||||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||||
|
|
||||||
# Remove the Docker image
|
# Remove the Docker image
|
||||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||||
|
|
||||||
echo "PERFORMED_CLEANUP"
|
echo "PERFORMED_CLEANUP"
|
||||||
else
|
else
|
||||||
echo "PR directory not found, nothing to clean up"
|
echo "PR directory not found, nothing to clean up"
|
||||||
@@ -51,7 +57,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
ENDSSH
|
ENDSSH
|
||||||
)
|
)
|
||||||
|
|
||||||
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
|
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
|
||||||
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
|
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
@@ -60,7 +66,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Post cleanup notice to PR
|
- name: Post cleanup notice to PR
|
||||||
if: steps.cleanup.outputs.cleanup_performed == 'true'
|
if: steps.cleanup.outputs.cleanup_performed == 'true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { GITHUB_REPOSITORY } = process.env;
|
const { GITHUB_REPOSITORY } = process.env;
|
||||||
|
|||||||
18
.github/workflows/auto-labeler.yml
vendored
18
.github/workflows/auto-labeler.yml
vendored
@@ -3,17 +3,23 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Apply Labels
|
- name: Apply Labels
|
||||||
uses: actions/labeler@v5
|
uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
configuration-path: .github/labeler-config.yml
|
configuration-path: .github/labeler-config.yml
|
||||||
|
|||||||
42
.github/workflows/build.yml
vendored
42
.github/workflows/build.yml
vendored
@@ -6,13 +6,13 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
actions: read
|
|
||||||
contents: read
|
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -21,23 +21,30 @@ jobs:
|
|||||||
jdk-version: [17, 21]
|
jdk-version: [17, 21]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.jdk-version }}
|
java-version: ${{ matrix.jdk-version }}
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Build with Gradle and no spring security
|
||||||
uses: gradle/actions/setup-gradle@v4
|
run: ./gradlew clean build
|
||||||
with:
|
env:
|
||||||
gradle-version: 8.7
|
DOCKER_ENABLE_SECURITY: false
|
||||||
|
|
||||||
- name: Build with Gradle
|
|
||||||
run: ./gradlew build --no-build-cache
|
|
||||||
|
|
||||||
|
- name: Build with Gradle and with spring security
|
||||||
|
run: ./gradlew clean build
|
||||||
|
env:
|
||||||
|
DOCKER_ENABLE_SECURITY: true
|
||||||
|
|
||||||
docker-compose-tests:
|
docker-compose-tests:
|
||||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||||
# (github.event_name == 'pull_request' &&
|
# (github.event_name == 'pull_request' &&
|
||||||
@@ -56,17 +63,22 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up Java 17
|
- name: Set up Java 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Install Docker Compose
|
- name: Install Docker Compose
|
||||||
run: |
|
run: |
|
||||||
@@ -74,7 +86,7 @@ jobs:
|
|||||||
sudo chmod +x /usr/local/bin/docker-compose
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
|
|
||||||
|
|||||||
257
.github/workflows/check_properties.yml
vendored
257
.github/workflows/check_properties.yml
vendored
@@ -1,42 +1,55 @@
|
|||||||
name: Check Properties Files
|
name: Check Properties Files on PR
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize, reopened]
|
types: [opened, synchronize, reopened]
|
||||||
paths:
|
paths:
|
||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
push:
|
|
||||||
branches: ["main"]
|
permissions:
|
||||||
paths:
|
contents: read # Allow read access to repository content
|
||||||
- "src/main/resources/messages_en_GB.properties"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-files:
|
check-files:
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write # Allow posting comments on issues/PRs
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout main branch first
|
- name: Harden Runner
|
||||||
uses: actions/checkout@v4
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
with:
|
with:
|
||||||
ref: main
|
egress-policy: audit
|
||||||
path: main-branch
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Checkout PR branch
|
- name: Checkout main branch first
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
path: pr-branch
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
|
|
||||||
- name: Install GitHub CLI
|
- name: Get PR data
|
||||||
run: sudo apt-get update && sudo apt-get install -y gh
|
id: get-pr-data
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const repoOwner = context.payload.repository.owner.login;
|
||||||
|
const repoName = context.payload.repository.name;
|
||||||
|
const branch = context.payload.pull_request.head.ref;
|
||||||
|
|
||||||
|
console.log(`PR Number: ${prNumber}`);
|
||||||
|
console.log(`Repo Owner: ${repoOwner}`);
|
||||||
|
console.log(`Repo Name: ${repoName}`);
|
||||||
|
console.log(`Branch: ${branch}`);
|
||||||
|
|
||||||
|
core.setOutput("pr_number", prNumber);
|
||||||
|
core.setOutput("repo_owner", repoOwner);
|
||||||
|
core.setOutput("repo_name", repoName);
|
||||||
|
core.setOutput("branch", branch);
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Fetch PR changed files
|
- name: Fetch PR changed files
|
||||||
id: fetch-pr-changes
|
id: fetch-pr-changes
|
||||||
@@ -44,45 +57,125 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "Fetching PR changed files..."
|
echo "Fetching PR changed files..."
|
||||||
cd pr-branch
|
|
||||||
gh repo set-default ${{ github.repository }}
|
|
||||||
# Store files in a safe way, only allowing valid properties files
|
|
||||||
echo "Getting list of changed files from PR..."
|
echo "Getting list of changed files from PR..."
|
||||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
|
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > changed_files.txt # Filter only matching property files
|
||||||
cd ..
|
|
||||||
|
|
||||||
echo "Processing changed files..."
|
- name: Determine reference file test
|
||||||
mapfile -t CHANGED_FILES < changed_files.txt
|
|
||||||
|
|
||||||
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
|
|
||||||
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
echo "Changed files: ${CHANGED_FILES_STR}"
|
|
||||||
|
|
||||||
- name: Determine reference file
|
|
||||||
id: determine-file
|
id: determine-file
|
||||||
run: |
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
echo "Determining reference file..."
|
with:
|
||||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
script: |
|
||||||
echo "Using PR branch reference file"
|
const fs = require("fs");
|
||||||
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
const path = require("path");
|
||||||
else
|
|
||||||
echo "Using main branch reference file"
|
|
||||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Show REFERENCE_FILE
|
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }};
|
||||||
run: echo "Reference file is set to ${REFERENCE_FILE}"
|
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}";
|
||||||
|
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
|
||||||
|
|
||||||
|
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
|
||||||
|
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
|
||||||
|
const branch = "${{ steps.get-pr-data.outputs.branch }}";
|
||||||
|
|
||||||
|
console.log(`Determining reference file for PR #${prNumber}`);
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
const validateInput = (input, regex, name) => {
|
||||||
|
if (!regex.test(input)) {
|
||||||
|
throw new Error(`Invalid ${name}: ${input}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
|
||||||
|
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
|
||||||
|
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
|
||||||
|
|
||||||
|
// Get the list of changed files in the PR
|
||||||
|
const { data: files } = await github.rest.pulls.listFiles({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
pull_number: prNumber,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter for relevant files based on the PR changes
|
||||||
|
const changedFiles = files
|
||||||
|
.map(file => file.filename)
|
||||||
|
.filter(file => /^src\/main\/resources\/messages_[a-zA-Z_]+\.properties$/.test(file));
|
||||||
|
|
||||||
|
console.log("Changed files:", changedFiles);
|
||||||
|
|
||||||
|
// Create a temporary directory for PR files
|
||||||
|
const tempDir = "pr-branch";
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and save each changed file
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: prRepoOwner,
|
||||||
|
repo: prRepoName,
|
||||||
|
path: file,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
const filePath = path.join(tempDir, file);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
console.log(`Saved file: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the list of changed files for further processing
|
||||||
|
const fileList = changedFiles.join(" ");
|
||||||
|
core.exportVariable("FILES_LIST", fileList);
|
||||||
|
console.log("Files saved and listed in FILES_LIST.");
|
||||||
|
|
||||||
|
// Determine reference file
|
||||||
|
let referenceFilePath;
|
||||||
|
if (changedFiles.includes("src/main/resources/messages_en_GB.properties")) {
|
||||||
|
console.log("Using PR branch reference file.");
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: prRepoOwner,
|
||||||
|
repo: prRepoName,
|
||||||
|
path: "src/main/resources/messages_en_GB.properties",
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "pr-branch-messages_en_GB.properties";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
} else {
|
||||||
|
console.log("Using main branch reference file.");
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
path: "src/main/resources/messages_en_GB.properties",
|
||||||
|
ref: "main",
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "main-branch-messages_en_GB.properties";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Reference file path: ${referenceFilePath}`);
|
||||||
|
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||||
|
|
||||||
- name: Run Python script to check files
|
- name: Run Python script to check files
|
||||||
id: run-check
|
id: run-check
|
||||||
run: |
|
run: |
|
||||||
echo "Running Python script to check files..."
|
echo "Running Python script to check files..."
|
||||||
python main-branch/.github/scripts/check_language_properties.py \
|
python .github/scripts/check_language_properties.py \
|
||||||
--actor ${{ github.event.pull_request.user.login }} \
|
--actor ${{ github.event.pull_request.user.login }} \
|
||||||
--reference-file "${REFERENCE_FILE}" \
|
--reference-file "${REFERENCE_FILE}" \
|
||||||
--branch pr-branch \
|
--branch "pr-branch" \
|
||||||
--files "${CHANGED_FILES[@]}" > result.txt || true
|
--files "${FILES_LIST[@]}" > result.txt
|
||||||
|
continue-on-error: true # Continue the job even if this step fails
|
||||||
|
|
||||||
- name: Capture output
|
- name: Capture output
|
||||||
id: capture-output
|
id: capture-output
|
||||||
@@ -95,7 +188,7 @@ jobs:
|
|||||||
echo "EOF" >> $GITHUB_ENV
|
echo "EOF" >> $GITHUB_ENV
|
||||||
echo "${SCRIPT_OUTPUT}"
|
echo "${SCRIPT_OUTPUT}"
|
||||||
|
|
||||||
# Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
|
# Determine job failure based on script output
|
||||||
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
|
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
|
||||||
echo "FAIL_JOB=true" >> $GITHUB_ENV
|
echo "FAIL_JOB=true" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
@@ -109,23 +202,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Post comment on PR
|
- name: Post comment on PR
|
||||||
if: env.SCRIPT_OUTPUT != ''
|
if: env.SCRIPT_OUTPUT != ''
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||||
const prNumber = context.issue.number;
|
const issueNumber = context.issue.number;
|
||||||
|
|
||||||
// Find existing comment
|
// Find existing comment
|
||||||
const comments = await github.rest.issues.listComments({
|
const comments = await github.rest.issues.listComments({
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
issue_number: prNumber
|
issue_number: issueNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
||||||
|
|
||||||
// Only allow the action user to update comments
|
// Only update or create comments by the action user
|
||||||
const expectedActor = "github-actions[bot]";
|
const expectedActor = "github-actions[bot]";
|
||||||
|
|
||||||
if (comment && comment.user.login === expectedActor) {
|
if (comment && comment.user.login === expectedActor) {
|
||||||
@@ -142,7 +235,7 @@ jobs:
|
|||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
issue_number: prNumber,
|
issue_number: issueNumber,
|
||||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||||
});
|
});
|
||||||
console.log("Created new comment.");
|
console.log("Created new comment.");
|
||||||
@@ -155,59 +248,3 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Failing the job because errors were detected."
|
echo "Failing the job because errors were detected."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
update-translations-main:
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
|
|
||||||
- name: Run Python script to check files
|
|
||||||
id: run-check
|
|
||||||
run: |
|
|
||||||
echo "Running Python script to check files..."
|
|
||||||
python .github/scripts/check_language_properties.py \
|
|
||||||
--reference-file src/main/resources/messages_en_GB.properties \
|
|
||||||
--branch main
|
|
||||||
|
|
||||||
- name: Set up git config
|
|
||||||
run: |
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Add translation keys
|
|
||||||
run: |
|
|
||||||
git add src/main/resources/messages_*.properties
|
|
||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: cpr
|
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
|
||||||
uses: peter-evans/create-pull-request@v7
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: "Update translation files"
|
|
||||||
committer: GitHub Action <action@github.com>
|
|
||||||
author: GitHub Action <action@github.com>
|
|
||||||
signoff: true
|
|
||||||
branch: update_translation_files
|
|
||||||
title: "Update translation files"
|
|
||||||
add-paths: |
|
|
||||||
src/main/resources/messages_*.properties
|
|
||||||
body: |
|
|
||||||
Auto-generated by [create-pull-request][1]
|
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
|
||||||
labels: Translation
|
|
||||||
draft: false
|
|
||||||
delete-branch: true
|
|
||||||
sign-commits: true
|
|
||||||
|
|||||||
79
.github/workflows/codeql.yml-disabled
vendored
Normal file
79
.github/workflows/codeql.yml-disabled
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
#disable for now
|
||||||
|
#on:
|
||||||
|
# push:
|
||||||
|
# branches: ["main"]
|
||||||
|
# pull_request:
|
||||||
|
# The branches below must be a subset of the branches above
|
||||||
|
# branches: ["main"]
|
||||||
|
# schedule:
|
||||||
|
# - cron: "0 0 * * 1"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ["java"]
|
||||||
|
# CodeQL supports [ $supported-codeql-languages ]
|
||||||
|
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||||
|
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
|
||||||
|
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||||
|
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||||
|
|
||||||
|
# - run: |
|
||||||
|
# echo "Run, Build Application using script"
|
||||||
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
27
.github/workflows/dependency-review.yml
vendored
Normal file
27
.github/workflows/dependency-review.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Dependency Review Action
|
||||||
|
#
|
||||||
|
# This Action will scan dependency manifest files that change as part of a Pull Request,
|
||||||
|
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
|
||||||
|
# Once installed, if the workflow run is marked as required,
|
||||||
|
# PRs introducing known-vulnerable packages will be blocked from merging.
|
||||||
|
#
|
||||||
|
# Source repository: https://github.com/actions/dependency-review-action
|
||||||
|
name: 'Dependency Review'
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
dependency-review:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: 'Checkout Repository'
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
|
||||||
26
.github/workflows/licenses-update.yml
vendored
26
.github/workflows/licenses-update.yml
vendored
@@ -7,25 +7,30 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "build.gradle"
|
- "build.gradle"
|
||||||
|
|
||||||
permissions:
|
permissions: read-all
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-license-report:
|
generate-license-report:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean generateLicenseReport
|
run: ./gradlew clean generateLicenseReport
|
||||||
@@ -47,7 +52,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@v6
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "Update 3rd Party Licenses"
|
commit-message: "Update 3rd Party Licenses"
|
||||||
@@ -60,9 +65,10 @@ jobs:
|
|||||||
Auto-generated by [create-pull-request][1]
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
labels: licenses
|
labels: licenses,github-actions
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
sign-commits: true
|
||||||
|
|
||||||
- name: Auto approve
|
- name: Auto approve
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
@@ -72,7 +78,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Enable auto-merge
|
- name: Enable auto-merge
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
uses: peter-evans/enable-pull-request-automerge@v3
|
uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
|||||||
17
.github/workflows/manage-label.yml
vendored
17
.github/workflows/manage-label.yml
vendored
@@ -4,21 +4,26 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "30 20 * * *"
|
- cron: "30 20 * * *"
|
||||||
|
|
||||||
permissions:
|
permissions: read-all
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
name: Labeler
|
name: Labeler
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Run Labeler
|
- name: Run Labeler
|
||||||
uses: crazy-max/ghaction-github-labeler@v5
|
uses: crazy-max/ghaction-github-labeler@b54af0c25861143e7c8813d7cbbf46d2c341680c # v5.1.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
yaml-file: .github/labels.yml
|
yaml-file: .github/labels.yml
|
||||||
skip-delete: true
|
skip-delete: true
|
||||||
|
|||||||
30
.github/workflows/multiOSReleases.yml
vendored
30
.github/workflows/multiOSReleases.yml
vendored
@@ -4,9 +4,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
permissions:
|
|
||||||
contents: write
|
permissions: read-all
|
||||||
packages: write
|
|
||||||
jobs:
|
jobs:
|
||||||
build-installers:
|
build-installers:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -22,17 +22,25 @@ jobs:
|
|||||||
# platform: linux
|
# platform: linux
|
||||||
# ext: deb
|
# ext: deb
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Set up JDK 21
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
@@ -42,7 +50,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
|
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
|
||||||
.\wix.exe /install /quiet
|
.\wix.exe /install /quiet
|
||||||
|
|
||||||
# Install Linux dependencies
|
# Install Linux dependencies
|
||||||
- name: Install Linux Dependencies
|
- name: Install Linux Dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
@@ -83,14 +91,14 @@ jobs:
|
|||||||
|
|
||||||
# Upload installer as artifact for testing
|
# Upload installer as artifact for testing
|
||||||
- name: Upload Installer Artifact
|
- name: Upload Installer Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
with:
|
with:
|
||||||
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
with:
|
with:
|
||||||
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
|
||||||
|
|||||||
54
.github/workflows/pre_commit.yml
vendored
Normal file
54
.github/workflows/pre_commit.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Pre-commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update:
|
||||||
|
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
- name: Run Pre-Commit Hooks
|
||||||
|
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Set up git config
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
- name: git add
|
||||||
|
run: |
|
||||||
|
git add .
|
||||||
|
git diff --staged --quiet || git commit -m ":file_folder: pre-commit
|
||||||
|
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: "ci: 🤖 format everything with pre-commit"
|
||||||
|
committer: GitHub Action <action@github.com>
|
||||||
|
author: GitHub Action <action@github.com>
|
||||||
|
signoff: true
|
||||||
|
branch: pre-commit
|
||||||
|
title: "🤖 format everything with pre-commit by <github-actions[bot]>"
|
||||||
|
body: |
|
||||||
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
labels: github-actions
|
||||||
|
sign-commits: true
|
||||||
79
.github/workflows/push-docker.yml
vendored
79
.github/workflows/push-docker.yml
vendored
@@ -10,20 +10,28 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
@@ -32,29 +40,35 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_ENABLE_SECURITY: false
|
DOCKER_ENABLE_SECURITY: false
|
||||||
|
|
||||||
|
- name: Install cosign
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
|
with:
|
||||||
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ github.token }}
|
password: ${{ github.token }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
|
||||||
|
|
||||||
- name: Convert repository owner to lowercase
|
- name: Convert repository owner to lowercase
|
||||||
id: repoowner
|
id: repoowner
|
||||||
@@ -62,7 +76,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate tags
|
- name: Generate tags
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
@@ -75,7 +89,8 @@ jobs:
|
|||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
uses: docker/build-push-action@v6
|
id: build-push-regular
|
||||||
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -87,10 +102,26 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
|
- name: Sign regular images
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
env:
|
||||||
|
DIGEST: ${{ steps.build-push-regular.outputs.digest }}
|
||||||
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||||
|
cosign sign --yes \
|
||||||
|
--key env://COSIGN_PRIVATE_KEY \
|
||||||
|
"${tag}@${DIGEST}"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Generate tags ultra-lite
|
- name: Generate tags ultra-lite
|
||||||
id: meta2
|
id: meta2
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@@ -103,11 +134,12 @@ jobs:
|
|||||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
uses: docker/build-push-action@v6
|
id: build-push-lite
|
||||||
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile-ultra-lite
|
file: ./Dockerfile.ultra-lite
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
@@ -115,10 +147,12 @@ jobs:
|
|||||||
labels: ${{ steps.meta2.outputs.labels }}
|
labels: ${{ steps.meta2.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
@@ -131,12 +165,13 @@ jobs:
|
|||||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
uses: docker/build-push-action@v6
|
id: build-push-fat
|
||||||
|
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.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 }}
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile-fat
|
file: ./Dockerfile.fat
|
||||||
push: true
|
push: true
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
@@ -144,3 +179,17 @@ jobs:
|
|||||||
labels: ${{ steps.meta3.outputs.labels }}
|
labels: ${{ steps.meta3.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
provenance: true
|
||||||
|
sbom: true
|
||||||
|
|
||||||
|
- name: Sign fat images
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
env:
|
||||||
|
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
|
||||||
|
TAGS: ${{ steps.meta3.outputs.tags }}
|
||||||
|
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
|
||||||
|
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
|
||||||
|
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
|
||||||
|
done
|
||||||
|
|||||||
70
.github/workflows/releaseArtifacts.yml
vendored
70
.github/workflows/releaseArtifacts.yml
vendored
@@ -4,12 +4,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
permissions:
|
|
||||||
contents: write
|
permissions: read-all
|
||||||
packages: write
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
enable_security: [true, false]
|
enable_security: [true, false]
|
||||||
@@ -19,15 +22,20 @@ jobs:
|
|||||||
- enable_security: false
|
- enable_security: false
|
||||||
file_suffix: ""
|
file_suffix: ""
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
@@ -45,16 +53,16 @@ jobs:
|
|||||||
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
- name: Upload Assets binarie
|
- name: Upload Assets binarie
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
with:
|
with:
|
||||||
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
overwrite: true
|
overwrite: true
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
with:
|
with:
|
||||||
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ jobs:
|
|||||||
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload Assets jar binaries
|
- name: Upload Assets jar binaries
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
with:
|
with:
|
||||||
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
@@ -71,46 +79,6 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload jar binaries to release
|
- name: Upload jar binaries to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
|
||||||
with:
|
with:
|
||||||
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
|
|
||||||
push-ui:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up JDK 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "temurin"
|
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
|
||||||
with:
|
|
||||||
gradle-version: 8.7
|
|
||||||
|
|
||||||
- name: Generate exe
|
|
||||||
run: ./gradlew clean createExe
|
|
||||||
env:
|
|
||||||
DOCKER_ENABLE_SECURITY: false
|
|
||||||
STIRLING_PDF_DESKTOP_UI: true
|
|
||||||
|
|
||||||
- name: Get version number
|
|
||||||
id: versionNumber
|
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload Assets binarie
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
path: ./build/launch4j/Stirling-PDF.exe
|
|
||||||
name: Stirling-PDF.exe
|
|
||||||
overwrite: true
|
|
||||||
retention-days: 1
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload binaries to release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
files: ./build/launch4j/Stirling-PDF.exe
|
|
||||||
|
|||||||
79
.github/workflows/scorecards.yml
vendored
Normal file
79
.github/workflows/scorecards.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# This workflow uses actions that are not certified by GitHub. They are provided
|
||||||
|
# by a third-party and are governed by separate terms of service, privacy
|
||||||
|
# policy, and support documentation.
|
||||||
|
|
||||||
|
name: Scorecard supply-chain security
|
||||||
|
on:
|
||||||
|
# For Branch-Protection check. Only the default branch is supported. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
|
||||||
|
branch_protection_rule:
|
||||||
|
# To guarantee Maintained check is occasionally updated. See
|
||||||
|
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
|
||||||
|
schedule:
|
||||||
|
- cron: '20 7 * * 2'
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analysis:
|
||||||
|
name: Scorecard analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# Needed to upload the results to code-scanning dashboard.
|
||||||
|
security-events: write
|
||||||
|
# Needed to publish results and get a badge (see publish_results below).
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
# To allow GraphQL ListCommits to work
|
||||||
|
issues: read
|
||||||
|
pull-requests: read
|
||||||
|
# To detect SAST tools
|
||||||
|
checks: read
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: "Checkout code"
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: "Run analysis"
|
||||||
|
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
||||||
|
with:
|
||||||
|
results_file: results.sarif
|
||||||
|
results_format: sarif
|
||||||
|
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
|
||||||
|
# - you want to enable the Branch-Protection check on a *public* repository, or
|
||||||
|
# - you are installing Scorecards on a *private* repository
|
||||||
|
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
|
||||||
|
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||||
|
|
||||||
|
# Public repositories:
|
||||||
|
# - Publish results to OpenSSF REST API for easy access by consumers
|
||||||
|
# - Allows the repository to include the Scorecard badge.
|
||||||
|
# - See https://github.com/ossf/scorecard-action#publishing-results.
|
||||||
|
# For private repositories:
|
||||||
|
# - `publish_results` will always be set to `false`, regardless
|
||||||
|
# of the value entered here.
|
||||||
|
publish_results: true
|
||||||
|
|
||||||
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
|
# format to the repository Actions tab.
|
||||||
|
- name: "Upload artifact"
|
||||||
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
|
with:
|
||||||
|
name: SARIF file
|
||||||
|
path: results.sarif
|
||||||
|
retention-days: 5
|
||||||
|
|
||||||
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
|
- name: "Upload to code-scanning"
|
||||||
|
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
|
||||||
|
with:
|
||||||
|
sarif_file: results.sarif
|
||||||
9
.github/workflows/stale.yml
vendored
9
.github/workflows/stale.yml
vendored
@@ -5,6 +5,8 @@ on:
|
|||||||
- cron: "30 0 * * *"
|
- cron: "30 0 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -12,8 +14,13 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
- name: 30 days stale issues
|
- name: 30 days stale issues
|
||||||
uses: actions/stale@v9
|
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 30
|
days-before-stale: 30
|
||||||
|
|||||||
13
.github/workflows/swagger.yml
vendored
13
.github/workflows/swagger.yml
vendored
@@ -6,19 +6,26 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|||||||
19
.github/workflows/sync_files.yml
vendored
19
.github/workflows/sync_files.yml
vendored
@@ -9,17 +9,23 @@ on:
|
|||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
- "scripts/ignore_translation.toml"
|
- "scripts/ignore_translation.toml"
|
||||||
|
|
||||||
permissions:
|
permissions: read-all
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-readme:
|
sync-readme:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -36,7 +42,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":memo: Sync README
|
git diff --staged --quiet || git commit -m ":memo: Sync README
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
@@ -52,3 +58,4 @@ jobs:
|
|||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
labels: Documentation,Translation,github-actions
|
labels: Documentation,Translation,github-actions
|
||||||
|
sign-commits: true
|
||||||
|
|||||||
71
.github/workflows/update-translations.yml
vendored
Normal file
71
.github/workflows/update-translations.yml
vendored
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
name: Update Translations
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "src/main/resources/messages_en_GB.properties"
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-translations-main:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
|
with:
|
||||||
|
python-version: "3.x"
|
||||||
|
|
||||||
|
- name: Run Python script to check files
|
||||||
|
id: run-check
|
||||||
|
run: |
|
||||||
|
echo "Running Python script to check files..."
|
||||||
|
python .github/scripts/check_language_properties.py \
|
||||||
|
--reference-file src/main/resources/messages_en_GB.properties \
|
||||||
|
--branch main
|
||||||
|
|
||||||
|
- name: Set up git config
|
||||||
|
run: |
|
||||||
|
git config --global user.name "github-actions[bot]"
|
||||||
|
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Add translation keys
|
||||||
|
run: |
|
||||||
|
git add src/main/resources/messages_*.properties
|
||||||
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Create Pull Request
|
||||||
|
id: cpr
|
||||||
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
commit-message: "Update translation files"
|
||||||
|
committer: GitHub Action <action@github.com>
|
||||||
|
author: GitHub Action <action@github.com>
|
||||||
|
signoff: true
|
||||||
|
branch: update_translation_files
|
||||||
|
title: "Update translation files"
|
||||||
|
add-paths: |
|
||||||
|
src/main/resources/messages_*.properties
|
||||||
|
body: |
|
||||||
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
draft: false
|
||||||
|
delete-branch: true
|
||||||
|
labels: Translation,github-actions
|
||||||
|
sign-commits: true
|
||||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -146,19 +146,37 @@ out/
|
|||||||
# cucumber
|
# cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
# Certs
|
# Certs and Security Files
|
||||||
*.p12
|
*.p12
|
||||||
|
*.pk8
|
||||||
*.pem
|
*.pem
|
||||||
*.crt
|
*.crt
|
||||||
*.cer
|
*.cer
|
||||||
|
*.cert
|
||||||
*.der
|
*.der
|
||||||
*.key
|
*.key
|
||||||
*.csr
|
*.csr
|
||||||
|
*.kdbx
|
||||||
|
*.jks
|
||||||
|
*.asc
|
||||||
|
|
||||||
|
# SSH Keys
|
||||||
|
*.pub
|
||||||
|
*.priv
|
||||||
|
id_rsa
|
||||||
|
id_rsa.pub
|
||||||
|
id_ecdsa
|
||||||
|
id_ecdsa.pub
|
||||||
|
id_ed25519
|
||||||
|
id_ed25519.pub
|
||||||
|
.ssh/
|
||||||
|
*ssh
|
||||||
|
|
||||||
# cache
|
# cache
|
||||||
|
.cache
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
**/jcef-bundle/
|
**/jcef-bundle/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.2.1
|
rev: v0.8.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@@ -12,7 +12,7 @@ repos:
|
|||||||
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
||||||
exclude: (split_photos.py)
|
exclude: (split_photos.py)
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
@@ -21,6 +21,25 @@ repos:
|
|||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
files: \.(properties|html|css|js|py|md)$
|
files: \.(properties|html|css|js|py|md)$
|
||||||
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
|
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.22.0
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
|
- repo: https://github.com/jumanjihouse/pre-commit-hooks
|
||||||
|
rev: 3.0.0
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
files: ^.*(\.bash|\.sh|\.ksh|\.zsh)$
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||||
|
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$)
|
||||||
|
- id: trailing-whitespace
|
||||||
|
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||||
|
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$)
|
||||||
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-duplicate-properties-keys
|
- id: check-duplicate-properties-keys
|
||||||
@@ -28,12 +47,11 @@ repos:
|
|||||||
entry: python .github/scripts/check_duplicates.py
|
entry: python .github/scripts/check_duplicates.py
|
||||||
language: python
|
language: python
|
||||||
files: ^(src)/.+\.properties$
|
files: ^(src)/.+\.properties$
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: check-html-tabs
|
- id: check-html-tabs
|
||||||
name: Check HTML for tabs
|
name: Check HTML for tabs
|
||||||
|
description: Ensures HTML/CSS/JS files do not contain tab characters
|
||||||
# args: ["--replace_with= "]
|
# args: ["--replace_with= "]
|
||||||
entry: python .github/scripts/check_tabulator.py
|
entry: python .github/scripts/check_tabulator.py
|
||||||
language: python
|
language: python
|
||||||
exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
|
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$)
|
||||||
files: ^.*(\.html|\.css|\.js)$
|
files: ^.*(\.html|\.css|\.js)$
|
||||||
@@ -18,9 +18,9 @@ For a detailed pull request tutorial, see [this guide](https://www.digitalocean.
|
|||||||
Please make sure your Pull Request adheres to the following guidelines:
|
Please make sure your Pull Request adheres to the following guidelines:
|
||||||
|
|
||||||
- Use the PR template provided.
|
- Use the PR template provided.
|
||||||
- Keep your Pull Request title succinct, detailed and to the point.
|
- Keep your Pull Request title succinct, detailed, and to the point.
|
||||||
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
|
||||||
- Commits should be clear, concise and easy to understand.
|
- Commits should be clear, concise, and easy to understand.
|
||||||
- References to the Issue number in the Pull Request and/or Commit message.
|
- References to the Issue number in the Pull Request and/or Commit message.
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
@@ -29,15 +29,15 @@ If you would like to add or modify a translation, please see [How to add new lan
|
|||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
|
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use the "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
|
||||||
|
|
||||||
## Fixing Bugs or Adding a New Feature
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
First, make sure you've read the section [Pull Requests](#pull-requests).
|
First, make sure you've read the section [Pull Requests](#pull-requests).
|
||||||
|
|
||||||
To build from source, please follow this [Guide](LocalRunGuide.md).
|
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
||||||
|
|
||||||
If, at any point of time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
|
Developers should review our [Developer Guide](DeveloperGuide.md)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
# New Database Backup and Import Functionality
|
# New Database Backup and Import Functionality
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **Full activation will take place on approximately January 5th, 2025!**
|
|
||||||
|
|
||||||
Why is the waiting time six months?
|
|
||||||
|
|
||||||
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
|
|
||||||
|
|
||||||
## Functionality Overview
|
## Functionality Overview
|
||||||
|
|
||||||
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. Introduction
|
## 1. Introduction
|
||||||
|
|
||||||
Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
|
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
|
||||||
|
|
||||||
## 2. Project Overview
|
## 2. Project Overview
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ Stirling-PDF is built using:
|
|||||||
- Docker
|
- Docker
|
||||||
- Git
|
- Git
|
||||||
- Java JDK 17 or later
|
- Java JDK 17 or later
|
||||||
- Gradle 7.0 or later (Included within repo)
|
- Gradle 7.0 or later (Included within the repo)
|
||||||
|
|
||||||
### Setup Steps
|
### Setup Steps
|
||||||
|
|
||||||
@@ -38,14 +38,14 @@ Stirling-PDF is built using:
|
|||||||
|
|
||||||
2. Install Docker and JDK17 if not already installed.
|
2. Install Docker and JDK17 if not already installed.
|
||||||
|
|
||||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ or VSCode
|
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||||
|
|
||||||
4. Lombok Setup
|
4. Lombok Setup
|
||||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||||
|
|
||||||
5. Add environment variable
|
5. Add environment variable
|
||||||
For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
|
For local testing, you should generally be testing the full 'Security' version of Stirling-PDF. To do this, you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step.
|
||||||
|
|
||||||
## 4. Project Structure
|
## 4. Project Structure
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ Stirling-PDF/
|
|||||||
│ └── SPDF/
|
│ └── SPDF/
|
||||||
├── build.gradle # Gradle build configuration
|
├── build.gradle # Gradle build configuration
|
||||||
├── Dockerfile # Main Dockerfile
|
├── Dockerfile # Main Dockerfile
|
||||||
├── Dockerfile-ultra-lite # Dockerfile for ultra-lite version
|
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||||
├── Dockerfile-fat # Dockerfile for fat version
|
├── Dockerfile.fat # Dockerfile for fat version
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||||
```
|
```
|
||||||
@@ -102,7 +102,7 @@ Stirling-PDF offers several Docker versions:
|
|||||||
|
|
||||||
### Example Docker Compose Files
|
### Example Docker Compose Files
|
||||||
|
|
||||||
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory such as :
|
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||||
|
|
||||||
- `docker-compose-latest.yml`: Latest version without security features
|
- `docker-compose-latest.yml`: Latest version without security features
|
||||||
- `docker-compose-latest-security.yml`: Latest version with security features enabled
|
- `docker-compose-latest-security.yml`: Latest version with security features enabled
|
||||||
@@ -179,14 +179,14 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
|||||||
For the ultra-lite version:
|
For the ultra-lite version:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||||
```
|
```
|
||||||
|
|
||||||
For the fat version (with security enabled):
|
For the fat version (with security enabled):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export DOCKER_ENABLE_SECURITY=true
|
export DOCKER_ENABLE_SECURITY=true
|
||||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
|
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||||
@@ -205,9 +205,9 @@ To run the test script:
|
|||||||
|
|
||||||
This script performs the following actions:
|
This script performs the following actions:
|
||||||
|
|
||||||
1. Builds all Docker images (full, ultra-lite, fat)
|
1. Builds all Docker images (full, ultra-lite, fat).
|
||||||
2. Runs each version to ensure it starts correctly
|
2. Runs each version to ensure it starts correctly.
|
||||||
3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
|
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||||
|
|
||||||
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
|
|||||||
|
|
||||||
To run Stirling-PDF locally:
|
To run Stirling-PDF locally:
|
||||||
|
|
||||||
1. Compile and run the project using built in IDE methods or by running:
|
1. Compile and run the project using built-in IDE methods or by running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
@@ -261,7 +261,7 @@ Important notes:
|
|||||||
|
|
||||||
6. Push your changes to your fork.
|
6. Push your changes to your fork.
|
||||||
7. Submit a pull request to the main repository.
|
7. Submit a pull request to the main repository.
|
||||||
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md).
|
||||||
|
|
||||||
When you raise a PR:
|
When you raise a PR:
|
||||||
|
|
||||||
@@ -317,7 +317,7 @@ Remember to test your changes thoroughly to ensure they don't break any existing
|
|||||||
|
|
||||||
### Overview of Thymeleaf
|
### Overview of Thymeleaf
|
||||||
|
|
||||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot
|
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||||
|
|
||||||
### Thymeleaf overview
|
### Thymeleaf overview
|
||||||
|
|
||||||
@@ -327,22 +327,24 @@ Some examples of this are:
|
|||||||
|
|
||||||
```html
|
```html
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
```
|
||||||
or
|
or
|
||||||
|
```html
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
```
|
```
|
||||||
|
|
||||||
Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside in generating the html, and block being the actual element type.
|
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||||
In this case we are inserting the ``navbar`` entry within the ``fragments/navbar.html`` fragment into the ``th:block`` element.
|
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||||
|
|
||||||
They can be more complex such as:
|
They can be more complex, such as:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||||
```
|
```
|
||||||
|
|
||||||
Which is the same as above but passes the parameters title and header into the fragment common.html to be used in its HTML generation
|
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||||
|
|
||||||
Thymeleaf can also be used to loop through objects or pass things from java side into html side.
|
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@@ -352,7 +354,7 @@ Thymeleaf can also be used to loop through objects or pass things from java side
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
in above example if exampleData is a list of plain java objects of class Person and within it you had id, name, age etc. You can reference it like so
|
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -452,7 +454,7 @@ This would generate n entries of tr for each person in exampleData
|
|||||||
1. **Create a New Thymeleaf Template:**
|
1. **Create a New Thymeleaf Template:**
|
||||||
- Create a new HTML file in the `src/main/resources/templates` directory.
|
- Create a new HTML file in the `src/main/resources/templates` directory.
|
||||||
- Use Thymeleaf attributes to dynamically generate content.
|
- Use Thymeleaf attributes to dynamically generate content.
|
||||||
- Use `extract-page.html` as a base example for the HTML template, useful to ensure importing of the general layout, navbar and footer.
|
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -10,6 +10,18 @@ COPY build/libs/*.jar app.jar
|
|||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="Stirling-PDF"
|
||||||
|
LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more."
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
|
||||||
|
LABEL org.opencontainers.image.licenses="MIT"
|
||||||
|
LABEL org.opencontainers.image.vendor="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
|
||||||
|
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
|
||||||
|
LABEL maintainer="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.authors="Stirling-Tools"
|
||||||
|
LABEL org.opencontainers.image.version="${VERSION_TAG}"
|
||||||
|
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
VERSION_TAG=$VERSION_TAG \
|
VERSION_TAG=$VERSION_TAG \
|
||||||
@@ -19,6 +31,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022
|
UMASK=022
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@testing 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 "@testing 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 && \
|
||||||
@@ -57,8 +70,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# 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
|
||||||
tesseract --list-langs
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build the application
|
# Build the application
|
||||||
FROM gradle:8.11-jdk17 AS build
|
FROM gradle:8.12-jdk17 AS build
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -73,8 +73,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# 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
|
||||||
tesseract --list-langs
|
|
||||||
|
|
||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.21.0
|
FROM alpine:3.21.0@sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
|
|
||||||
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
|
|
||||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
|
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| crop | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| extract-page | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | |
|
|
||||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
|
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | ✔️ | | ✔️ | | | | ✔️ | |
|
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
|
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
|
||||||
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
|
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
|
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
|
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
|
||||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
|
||||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
|
||||||
| add-password | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | | | |
|
|
||||||
| add-image | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ | | |
|
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
|
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
|
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
|
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
|
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
|
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ | | |
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
## User Guide for Local Directory Scanning and File Processing
|
|
||||||
|
|
||||||
### Setting Up Watched Folders
|
|
||||||
|
|
||||||
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
|
||||||
- The default directory for this is `./pipeline/watchedFolders/`.
|
|
||||||
- Place any directories you want to be scanned into this folder. This folder should contain multiple folders, each for their own tasks and pipelines.
|
|
||||||
|
|
||||||
### Configuring Processing with JSON Files
|
|
||||||
|
|
||||||
- In each directory you want processed (e.g., `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
|
|
||||||
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them). This can be made, configured, and downloaded from the Stirling-PDF Pipeline interface.
|
|
||||||
|
|
||||||
### Automatic Scanning and Processing
|
|
||||||
|
|
||||||
- The system automatically checks the watched folder every minute for new directories and files to process.
|
|
||||||
- When a directory with a valid JSON configuration file is found, it begins processing the files inside according to the configuration.
|
|
||||||
|
|
||||||
### Processing Steps
|
|
||||||
|
|
||||||
- Files in each directory are processed according to the instructions in the JSON file.
|
|
||||||
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to the next process.
|
|
||||||
|
|
||||||
### Results and Output
|
|
||||||
|
|
||||||
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
|
|
||||||
- Each processed file is named and organized according to the rules set in the JSON configuration.
|
|
||||||
|
|
||||||
### Completion and Cleanup
|
|
||||||
|
|
||||||
- Once processing is complete, the original files in the watched folder's directory are removed.
|
|
||||||
- You can find the processed files in the designated output location.
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
|
|
||||||
|
|
||||||
### User Interaction
|
|
||||||
|
|
||||||
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
|
|
||||||
- The system handles the rest, including scanning, processing, and outputting results.
|
|
||||||
@@ -92,8 +92,9 @@ Verify installation:
|
|||||||
``tesseract --list-langs``
|
``tesseract --list-langs``
|
||||||
|
|
||||||
You must then edit your ``/configs/settings.yml`` and change the system.tessdataDir to match the directory containing lang files
|
You must then edit your ``/configs/settings.yml`` and change the system.tessdataDir to match the directory containing lang files
|
||||||
|
|
||||||
```
|
```
|
||||||
system:
|
system:
|
||||||
tessdataDir: C:/Program Files/Tesseract-OCR/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
tessdataDir: C:/Program Files/Tesseract-OCR/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
||||||
```
|
```
|
||||||
|
|
||||||
327
LocalRunGuide.md
327
LocalRunGuide.md
@@ -1,327 +0,0 @@
|
|||||||
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
|
|
||||||
|
|
||||||
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
|
|
||||||
|
|
||||||
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
|
|
||||||
|
|
||||||
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. The amount of dependencies is to actually reduce overall size, i.e., installing LibreOffice subcomponents rather than the full LibreOffice package.
|
|
||||||
|
|
||||||
You could theoretically use a Distrobox/Toolbox if your distribution has old or not all packages. But you might just as well use the Docker container then.
|
|
||||||
|
|
||||||
### Step 1: Prerequisites
|
|
||||||
|
|
||||||
Install the following software, if not already installed:
|
|
||||||
|
|
||||||
- Java 17 or later (21 recommended)
|
|
||||||
- Gradle 7.0 or later (included within repo so not needed on server)
|
|
||||||
- Git
|
|
||||||
- Python 3.8 (with pip)
|
|
||||||
- Make
|
|
||||||
- GCC/G++
|
|
||||||
- Automake
|
|
||||||
- Autoconf
|
|
||||||
- libtool
|
|
||||||
- pkg-config
|
|
||||||
- zlib1g-dev
|
|
||||||
- libleptonica-dev
|
|
||||||
|
|
||||||
For Debian-based systems, you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
|
|
||||||
```
|
|
||||||
|
|
||||||
For Fedora-based systems use this command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip
|
|
||||||
```
|
|
||||||
|
|
||||||
For non-root users with Nix Package Manager, use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix-channel --update
|
|
||||||
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
|
|
||||||
|
|
||||||
For Debian and Fedora, you can build it from source using the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir ~/.git
|
|
||||||
cd ~/.git && \
|
|
||||||
git clone https://github.com/agl/jbig2enc.git && \
|
|
||||||
cd jbig2enc && \
|
|
||||||
./autogen.sh && \
|
|
||||||
./configure && \
|
|
||||||
make && \
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix-env -iA nixpkgs.jbig2enc
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Install Additional Software
|
|
||||||
|
|
||||||
Next we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
|
|
||||||
|
|
||||||
Install the following software:
|
|
||||||
|
|
||||||
- libreoffice-core
|
|
||||||
- libreoffice-common
|
|
||||||
- libreoffice-writer
|
|
||||||
- libreoffice-calc
|
|
||||||
- libreoffice-impress
|
|
||||||
- python3-uno
|
|
||||||
- unoconv
|
|
||||||
- pngquant
|
|
||||||
- unpaper
|
|
||||||
- qpdf
|
|
||||||
- opencv-python-headless
|
|
||||||
|
|
||||||
For Debian-based systems, you can use the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
|
||||||
```
|
|
||||||
|
|
||||||
For Fedora:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
|
||||||
```
|
|
||||||
|
|
||||||
For Nix:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
|
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Clone and Build Stirling-PDF
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ~/.git && \
|
|
||||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && \
|
|
||||||
cd Stirling-PDF && \
|
|
||||||
chmod +x ./gradlew && \
|
|
||||||
./gradlew build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5: Move Jar to Desired Location
|
|
||||||
|
|
||||||
After the build process, a `.jar` file will be generated in the `build/libs` directory. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. This folder is required for the Python scripts using OpenCV.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo mkdir /opt/Stirling-PDF && \
|
|
||||||
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ && \
|
|
||||||
sudo mv scripts /opt/Stirling-PDF/ && \
|
|
||||||
echo "Scripts installed."
|
|
||||||
```
|
|
||||||
|
|
||||||
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6: Other Files
|
|
||||||
|
|
||||||
#### OCR
|
|
||||||
|
|
||||||
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-English scanning.
|
|
||||||
|
|
||||||
##### Installing Language Packs
|
|
||||||
|
|
||||||
The easiest method is to use the language packs provided by your repositories. Skip the other steps if they are available.
|
|
||||||
|
|
||||||
**Manual:**
|
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
|
||||||
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
|
||||||
|
|
||||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
|
||||||
|
|
||||||
**Debian-based systems**, install languages with this command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update && \
|
|
||||||
# All languages
|
|
||||||
# sudo apt install -y 'tesseract-ocr-*'
|
|
||||||
|
|
||||||
# Find languages:
|
|
||||||
apt search tesseract-ocr-
|
|
||||||
|
|
||||||
# View installed languages:
|
|
||||||
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fedora:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All languages
|
|
||||||
# sudo dnf install -y tesseract-langpack-*
|
|
||||||
|
|
||||||
# Find languages:
|
|
||||||
dnf search -C tesseract-langpack-
|
|
||||||
|
|
||||||
# View installed languages:
|
|
||||||
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Nix:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix-env -iA nixpkgs.tesseract
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note:** Nix Package Manager pre-installs almost all the language packs when Tesseract is installed.
|
|
||||||
|
|
||||||
### Step 7: Run Stirling-PDF
|
|
||||||
|
|
||||||
Those who have pushed to the root directory, run the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew bootRun
|
|
||||||
or
|
|
||||||
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
Since LibreOffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
|
|
||||||
|
|
||||||
```
|
|
||||||
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory ‘/run/user/1501’: Permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
To resolve this, before starting Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir temp
|
|
||||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
|
|
||||||
./gradlew bootRun
|
|
||||||
or
|
|
||||||
java -jar ./Stirling-PDF-*.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 8: Adding a Desktop Icon
|
|
||||||
|
|
||||||
This will add a modified app starter to your app menu.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
location=$(pwd)/gradlew
|
|
||||||
image=$(pwd)/docs/stirling-transparent.svg
|
|
||||||
|
|
||||||
cat > ~/.local/share/applications/Stirling-PDF.desktop <<EOF
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Stirling PDF;
|
|
||||||
GenericName=Launch StirlingPDF and open its WebGUI;
|
|
||||||
Category=Office;
|
|
||||||
Exec=xdg-open http://localhost:8080 && nohup $location bootRun &;
|
|
||||||
Icon=$image;
|
|
||||||
Keywords=pdf;
|
|
||||||
Type=Application;
|
|
||||||
NoDisplay=false;
|
|
||||||
Terminal=true;
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Currently the app will run in the background until manually closed.
|
|
||||||
|
|
||||||
### Optional: Changing the Host and Port of the Application
|
|
||||||
|
|
||||||
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
server:
|
|
||||||
host: 0.0.0.0 # Not working - use instead address
|
|
||||||
address: 0.0.0.0
|
|
||||||
port: 3000
|
|
||||||
```
|
|
||||||
|
|
||||||
`-Djava.net.preferIPv4Stack=true` --> To force IPv4 only in the Java starting command
|
|
||||||
|
|
||||||
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
|
|
||||||
|
|
||||||
### Optional: Run Stirling-PDF as a Service (requires root)
|
|
||||||
|
|
||||||
First create a `.env` file, where you can store environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
touch /opt/Stirling-PDF/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
In this file, you can add all variables, one variable per line, as stated in the main readme (for example `SYSTEM_DEFAULTLOCALE="de-DE"`).
|
|
||||||
|
|
||||||
Create a new file where we store our service settings and open it with the nano editor:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano /etc/systemd/system/stirlingpdf.service
|
|
||||||
```
|
|
||||||
|
|
||||||
Paste this content, make sure to update the filename of the jar file. Press `Ctrl+S` and `Ctrl+X` to save and exit the nano editor:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Stirling-PDF service
|
|
||||||
After=syslog.target network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
SuccessExitStatus=143
|
|
||||||
|
|
||||||
User=root
|
|
||||||
Group=root
|
|
||||||
|
|
||||||
Type=simple
|
|
||||||
|
|
||||||
EnvironmentFile=/opt/Stirling-PDF/.env
|
|
||||||
WorkingDirectory=/opt/Stirling-PDF
|
|
||||||
ExecStart=/usr/bin/java -jar Stirling-PDF-0.17.2.jar
|
|
||||||
ExecStop=/bin/kill -15 $MAINPID
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
```
|
|
||||||
|
|
||||||
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
```
|
|
||||||
|
|
||||||
Enable the service to tell it to start automatically:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl enable stirlingpdf.service
|
|
||||||
```
|
|
||||||
|
|
||||||
See the status of the service:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status stirlingpdf.service
|
|
||||||
```
|
|
||||||
|
|
||||||
Manually start/stop/restart the service:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl start stirlingpdf.service
|
|
||||||
sudo systemctl stop stirlingpdf.service
|
|
||||||
sudo systemctl restart stirlingpdf.service
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Remember to set the necessary environment variables before running the project if you want to customize the application. The list can be seen in the main readme.
|
|
||||||
|
|
||||||
You can do this in the terminal by using the `export` command or `-D` argument to the Java `-jar` command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export APP_HOME_NAME="Stirling PDF"
|
|
||||||
or
|
|
||||||
-DAPP_HOME_NAME="Stirling PDF"
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Pipeline Configuration and Usage Tutorial
|
|
||||||
|
|
||||||
- Configure the pipeline config file and input files to run files against it.
|
|
||||||
- For reuse, download the config file and re-upload it when needed, or place it in `/pipeline/defaultWebUIConfigs/` to auto-load in the web UI for all users.
|
|
||||||
|
|
||||||
## Steps to Configure and Use Your Pipeline
|
|
||||||
|
|
||||||
1. **Access Configuration**
|
|
||||||
- Upon entering the screen, click on the **Configure** button.
|
|
||||||
|
|
||||||
2. **Enter Pipeline Name**
|
|
||||||
- Provide a name for your pipeline in the designated field.
|
|
||||||
|
|
||||||
3. **Select Operations**
|
|
||||||
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
|
|
||||||
|
|
||||||
4. **Configure Operation Settings**
|
|
||||||
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
|
|
||||||
|
|
||||||
5. **Add More Operations**
|
|
||||||
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
|
|
||||||
|
|
||||||
6. **Save Settings**
|
|
||||||
- Click **Save Operation Settings** after customizing settings for each operation.
|
|
||||||
|
|
||||||
7. **Validate Pipeline**
|
|
||||||
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
|
|
||||||
|
|
||||||
8. **Download Pipeline Configuration**
|
|
||||||
- To use the configuration for folder scanning (or save it for future use and re-upload it), download a JSON file in this menu. You can also pre-load it for future use by placing it in `/pipeline/defaultWebUIConfigs/`. It will then appear in the dropdown menu for all users to use.
|
|
||||||
|
|
||||||
9. **Submit Files for Processing**
|
|
||||||
- If your pipeline is correctly set up, close the configure menu, input the files, and hit **Submit**.
|
|
||||||
|
|
||||||
10. **Note on Web UI Limitations**
|
|
||||||
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
|
||||||
|
|
||||||
### Current Limitations
|
|
||||||
|
|
||||||
- Cannot have more than one of the same operation.
|
|
||||||
- Cannot input additional files via UI.
|
|
||||||
- All files and operations run in serial mode.
|
|
||||||
374
README.md
374
README.md
@@ -6,34 +6,36 @@
|
|||||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
|
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
|
|
||||||
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
||||||
|
|
||||||
Stirling-PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
|
||||||
|
|
||||||
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
||||||
|
|
||||||
|
|
||||||
|
All information available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
- 50+ PDF Operations
|
||||||
|
- Parallel file processing and downloads
|
||||||
- Dark mode support
|
- Dark mode support
|
||||||
- Custom download options
|
- Custom download options
|
||||||
- Parallel file processing and downloads
|
- Custom 'Pipelines' to run multiple features in a automated queue
|
||||||
- Custom 'Pipelines' to run multiple features in a queue
|
|
||||||
- API for integration with external scripts
|
- API for integration with external scripts
|
||||||
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
|
||||||
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
||||||
|
- Enterprise features like SSO see [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||||
|
|
||||||
## PDF Features
|
## PDF Features
|
||||||
|
|
||||||
### Page Operations
|
### Page Operations
|
||||||
|
|
||||||
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus on-page edit features like annotate, draw, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
|
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus, on-page edit features like annotating, drawing, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
|
||||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
|
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
|
||||||
- Merge multiple PDFs into a single resultant file
|
- Merge multiple PDFs into a single resultant file
|
||||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
|
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
|
||||||
@@ -44,11 +46,11 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Scale page contents size by set percentage
|
- Scale page contents size by set percentage
|
||||||
- Adjust contrast
|
- Adjust contrast
|
||||||
- Crop PDF
|
- Crop PDF
|
||||||
- Auto split PDF (with physically scanned page dividers)
|
- Auto-split PDF (with physically scanned page dividers)
|
||||||
- Extract page(s)
|
- Extract page(s)
|
||||||
- Convert PDF to a single page
|
- Convert PDF to a single page
|
||||||
- Overlay PDFs on top of each other
|
- Overlay PDFs on top of each other
|
||||||
- PDF to single page
|
- PDF to a single page
|
||||||
- Split PDF by sections
|
- Split PDF by sections
|
||||||
|
|
||||||
### Conversion Operations
|
### Conversion Operations
|
||||||
@@ -57,7 +59,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Convert any common file to PDF (using LibreOffice)
|
- Convert any common file to PDF (using LibreOffice)
|
||||||
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
|
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
|
||||||
- Convert HTML to PDF
|
- Convert HTML to PDF
|
||||||
- Convert PDF to xml
|
- Convert PDF to XML
|
||||||
- Convert PDF to CSV
|
- Convert PDF to CSV
|
||||||
- URL to PDF
|
- URL to PDF
|
||||||
- Markdown to PDF
|
- Markdown to PDF
|
||||||
@@ -85,105 +87,28 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Extract images from scans
|
- Extract images from scans
|
||||||
- Remove annotations
|
- Remove annotations
|
||||||
- Add page numbers
|
- Add page numbers
|
||||||
- Auto rename file by detecting PDF header text
|
- Auto-rename files by detecting PDF header text
|
||||||
- OCR on PDF (using tesseract)
|
- OCR on PDF (using Tesseract OCR)
|
||||||
- PDF/A conversion (using libreoffice)
|
- PDF/A conversion (using LibreOffice)
|
||||||
- Edit metadata
|
- Edit metadata
|
||||||
- Flatten PDFs
|
- Flatten PDFs
|
||||||
- Get all information on a PDF to view or export as JSON
|
- Get all information on a PDF to view or export as JSON
|
||||||
- Show/detect embedded JavaScript
|
- Show/detect embedded JavaScript
|
||||||
|
|
||||||
For an overview of the tasks and the technology each uses, please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
|
|
||||||
|
|
||||||
A demo of the app is available [here](https://stirlingpdf.io).
|
|
||||||
|
|
||||||
## Technologies Used
|
|
||||||
|
|
||||||
- Spring Boot + Thymeleaf
|
# 📖 Get Started
|
||||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
|
||||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
|
||||||
- [qpdf](https://github.com/qpdf/qpdf)
|
|
||||||
- HTML, CSS, JavaScript
|
|
||||||
- Docker
|
|
||||||
- [PDF.js](https://github.com/mozilla/pdf.js)
|
|
||||||
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
|
|
||||||
|
|
||||||
## How to Use
|
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for:
|
||||||
|
|
||||||
### Windows
|
- Installation guides for all platforms
|
||||||
|
- Configuration options
|
||||||
|
- Feature documentation
|
||||||
|
- API reference
|
||||||
|
- Security setup
|
||||||
|
- Enterprise features
|
||||||
|
|
||||||
For Windows users, download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe).
|
|
||||||
|
|
||||||
### Locally
|
|
||||||
|
|
||||||
Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md).
|
|
||||||
|
|
||||||
### Docker / Podman
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
|
|
||||||
|
|
||||||
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
|
|
||||||
|
|
||||||
### Docker Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
-p 8080:8080 \
|
|
||||||
-v ./trainingData:/usr/share/tessdata \
|
|
||||||
-v ./extraConfigs:/configs \
|
|
||||||
-v ./logs:/logs \
|
|
||||||
# Optional customization (not required)
|
|
||||||
# -v /location/of/customFiles:/customFiles \
|
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
|
||||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
|
||||||
-e LANGS=en_GB \
|
|
||||||
--name stirling-pdf \
|
|
||||||
stirlingtools/stirling-pdf:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
version: '3.3'
|
|
||||||
services:
|
|
||||||
stirling-pdf:
|
|
||||||
image: stirlingtools/stirling-pdf:latest
|
|
||||||
ports:
|
|
||||||
- '8080:8080'
|
|
||||||
volumes:
|
|
||||||
- ./trainingData:/usr/share/tessdata # Required for extra OCR languages
|
|
||||||
- ./extraConfigs:/configs
|
|
||||||
# - ./customFiles:/customFiles/
|
|
||||||
# - ./logs:/logs/
|
|
||||||
environment:
|
|
||||||
- DOCKER_ENABLE_SECURITY=false
|
|
||||||
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
|
||||||
- LANGS=en_GB
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
|
||||||
|
|
||||||
### Kubernetes
|
|
||||||
|
|
||||||
See the kubernetes helm chart [here](https://github.com/Stirling-Tools/Stirling-PDF-chart)
|
|
||||||
|
|
||||||
## Enable OCR/Compression Feature
|
|
||||||
|
|
||||||
Please view the [HowToUseOCR.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md).
|
|
||||||
|
|
||||||
## Reuse Stored Files
|
|
||||||
|
|
||||||
Certain functionality like `Sign` supports pre-saved files stored at `/customFiles/signatures/`. Image files placed within here will be accessible to be used via the web UI. Currently, this supports two folder types:
|
|
||||||
|
|
||||||
- `/customFiles/signatures/ALL_USERS`: Accessible to all users, useful for organizations where many users use the same files or for users not using authentication
|
|
||||||
- `/customFiles/signatures/{username}`: Such as `/customFiles/signatures/froodle`, accessible only to the `froodle` username, private for all others
|
|
||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
@@ -191,238 +116,57 @@ Stirling-PDF currently supports 38 languages!
|
|||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| -------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| 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) |  |
|
||||||
|
|
||||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
|
||||||
|
|
||||||
Please see our [Contributing Guide](CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Stirling PDF Enterprise
|
## Stirling PDF Enterprise
|
||||||
|
|
||||||
Stirling PDF offers a Enterprise edition of its software, This is the same great software but with added features and comforts
|
Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features, support and comforts.
|
||||||
|
Check out our [Enterprise docs](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||||
### Whats included
|
|
||||||
|
|
||||||
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
|
|
||||||
- Prioritised Enhancements to Stirling-PDF where applicable
|
|
||||||
- Base SSO support
|
|
||||||
- Advanced SSO such as automated login handling (Coming very soon)
|
|
||||||
- SAML SSO (Coming very soon)
|
|
||||||
- Custom automated metadata handling
|
|
||||||
- Advanced user configurations (Coming soon)
|
|
||||||
- Plus other exciting features to come
|
|
||||||
|
|
||||||
Check out of [docs](https://docs.stirlingpdf.com/Enterprise%20Edition) on it or our official [website](https://www.stirlingpdf.com)
|
|
||||||
|
|
||||||
## Customization
|
|
||||||
|
|
||||||
Stirling-PDF allows easy customization of the app, including things like:
|
|
||||||
|
|
||||||
- Custom application name
|
|
||||||
- Custom slogans, icons, HTML, images, CSS, etc. (via file overrides)
|
|
||||||
|
|
||||||
There are two options for this, either using the generated settings file `settings.yml`, which is located in the `/configs` directory and follows standard YAML formatting, or using environment variables, which would override the settings file.
|
|
||||||
|
|
||||||
For example, in `settings.yml`, you might have:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
security:
|
|
||||||
enableLogin: 'true'
|
|
||||||
```
|
|
||||||
|
|
||||||
To have this via an environment variable, you would use `SECURITY_ENABLELOGIN`.
|
|
||||||
|
|
||||||
The current list of settings is:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
security:
|
|
||||||
enableLogin: false # set to 'true' to enable login
|
|
||||||
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
|
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
|
||||||
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
|
||||||
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
|
|
||||||
initialLogin:
|
|
||||||
username: '' # initial username for the first login
|
|
||||||
password: '' # initial password for the first login
|
|
||||||
oauth2:
|
|
||||||
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
|
||||||
client:
|
|
||||||
keycloak:
|
|
||||||
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
|
||||||
clientId: '' # client ID for Keycloak OAuth2
|
|
||||||
clientSecret: '' # client secret for Keycloak OAuth2
|
|
||||||
scopes: openid, profile, email # scopes for Keycloak OAuth2
|
|
||||||
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2
|
|
||||||
google:
|
|
||||||
clientId: '' # client ID for Google OAuth2
|
|
||||||
clientSecret: '' # client secret for Google OAuth2
|
|
||||||
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
|
|
||||||
useAsUsername: email # field to use as the username for Google OAuth2
|
|
||||||
github:
|
|
||||||
clientId: '' # client ID for GitHub OAuth2
|
|
||||||
clientSecret: '' # client secret for GitHub OAuth2
|
|
||||||
scopes: read:user # scope for GitHub OAuth2
|
|
||||||
useAsUsername: login # field to use as the username for GitHub OAuth2
|
|
||||||
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
|
|
||||||
clientId: '' # client ID from your provider
|
|
||||||
clientSecret: '' # client secret from your provider
|
|
||||||
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
|
||||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
|
||||||
useAsUsername: email # default is 'email'; custom fields can be used as the username
|
|
||||||
scopes: openid, profile, email # specify the scopes for which the application will request permissions
|
|
||||||
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
|
||||||
saml2:
|
|
||||||
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
|
|
||||||
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
|
||||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
|
||||||
registrationId: stirling
|
|
||||||
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
|
|
||||||
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
|
|
||||||
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
|
|
||||||
idpIssuer: http://www.okta.com/externalKey
|
|
||||||
idpCert: classpath:okta.crt
|
|
||||||
privateKey: classpath:saml-private-key.key
|
|
||||||
spCert: classpath:saml-public-cert.crt
|
|
||||||
|
|
||||||
enterpriseEdition:
|
|
||||||
enabled: false # set to 'true' to enable enterprise edition
|
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
|
||||||
CustomMetadata:
|
|
||||||
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
|
||||||
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
|
|
||||||
creator: Stirling-PDF # supports text such as 'Company-PDF'
|
|
||||||
producer: Stirling-PDF # supports text such as 'Company-PDF'
|
|
||||||
|
|
||||||
legal:
|
|
||||||
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
|
|
||||||
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
|
|
||||||
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
|
|
||||||
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
|
|
||||||
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
|
|
||||||
|
|
||||||
system:
|
|
||||||
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
|
||||||
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
|
||||||
showUpdate: false # see when a new update is available
|
|
||||||
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
|
||||||
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
|
||||||
enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
|
|
||||||
|
|
||||||
ui:
|
|
||||||
appName: '' # application's visible name
|
|
||||||
homeDescription: '' # short description or tagline shown on the homepage
|
|
||||||
appNameNavbar: '' # name displayed on the navigation bar
|
|
||||||
|
|
||||||
endpoints:
|
|
||||||
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
|
||||||
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
|
||||||
|
|
||||||
metrics:
|
|
||||||
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
|
||||||
|
|
||||||
# Automatically Generated Settings (Do Not Edit Directly)
|
|
||||||
AutomaticallyGenerated:
|
|
||||||
key: example
|
|
||||||
UUID: example
|
|
||||||
```
|
|
||||||
|
|
||||||
There is an additional config file `/configs/custom_settings.yml` where users familiar with Java and Spring `application.properties` can input their own settings on top of Stirling-PDF's existing ones.
|
|
||||||
|
|
||||||
|
|
||||||
|
## 🤝 Looking to contribute?
|
||||||
|
|
||||||
### Extra Notes
|
Join our community:
|
||||||
|
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||||
- **Endpoints**: Currently, the `ENDPOINTS_TO_REMOVE` and `GROUPS_TO_REMOVE` endpoints can include comma-separated lists of endpoints and groups to disable. For example, `ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages` would disable both image-to-pdf and remove pages, while `GROUPS_TO_REMOVE=LibreOffice` would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
|
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md)
|
||||||
- **customStaticFilePath**: Customize static files such as the app logo by placing files in the `/customFiles/static/` directory. An example of customizing the app logo is placing `/customFiles/static/favicon.svg` to override the current SVG. This can be used to change any `images/icons/css/fonts/js`, etc. in Stirling-PDF.
|
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||||
|
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||||
### Environment-Only Parameters
|
- [Developer Guide](DeveloperGuide.md)
|
||||||
|
|
||||||
- `SYSTEM_ROOTURIPATH` - Set the application's root URI (e.g. `/pdf-app` to set the root URI to `localhost:8080/pdf-app`)
|
|
||||||
- `SYSTEM_CONNECTIONTIMEOUTMINUTES` - Set custom connection timeout values
|
|
||||||
- `DOCKER_ENABLE_SECURITY` - Set to `true` to download security jar (required for authentication login)
|
|
||||||
- `INSTALL_BOOK_AND_ADVANCED_HTML_OPS` - Download Calibre onto Stirling-PDF to enable PDF to/from book and advanced HTML conversion
|
|
||||||
- `LANGS` - Define custom font libraries to install for document conversions
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
For those wanting to use Stirling-PDF's backend API to link with their own custom scripting to edit PDFs, you can view all existing API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/), or navigate to `/swagger-ui/index.html` of your Stirling-PDF instance for your version's documentation (or by following the API button in the settings of Stirling-PDF).
|
|
||||||
|
|
||||||
## Login Authentication
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- User must have the folder `./configs` volumed within Docker so that it is retained during updates.
|
|
||||||
- Docker users must download the security jar version by setting `DOCKER_ENABLE_SECURITY` to `true` in environment variables.
|
|
||||||
- Then either enable login via the `settings.yml` file or set `SECURITY_ENABLE_LOGIN` to `true`.
|
|
||||||
- Now the initial user will be generated with username `admin` and password `stirling`. On login, you will be forced to change the password to a new one. You can also use the environment variables `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` to set your own credentials straight away (recommended to remove them after user creation).
|
|
||||||
|
|
||||||
Once the above has been done, on restart, a new `stirling-pdf-DB.mv.db` will show if everything worked.
|
|
||||||
|
|
||||||
When you log in to Stirling-PDF, you will be redirected to the `/login` page to log in with those default credentials. After login, everything should function as normal.
|
|
||||||
|
|
||||||
To access your account settings, go to Account Settings in the settings cog menu (top right in the navbar). This Account Settings menu is also where you find your API key.
|
|
||||||
|
|
||||||
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
|
|
||||||
|
|
||||||
For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user.
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
### Q1: What are your planned features?
|
|
||||||
|
|
||||||
- Progress bar/tracking
|
|
||||||
- Full custom logic pipelines to combine multiple operations together
|
|
||||||
- Folder support with auto-scanning to perform operations on
|
|
||||||
- Redact text (via UI, not just automated)
|
|
||||||
- Add forms
|
|
||||||
- Multi-page layout (stitch PDF pages together) support x rows y columns and custom page sizing
|
|
||||||
- Fill forms manually or automatically
|
|
||||||
|
|
||||||
### Q2: Why is my application downloading .htm files? Why am i getting HTTP error 413?
|
|
||||||
|
|
||||||
This is an issue commonly caused by your NGINX configuration. The default file upload size for NGINX is 1MB. You need to add the following in your Nginx sites-available file: `client_max_body_size SIZE;` (where "SIZE" is 50M for example for 50MB files).
|
|
||||||
|
|
||||||
### Q3: Why is my download timing out?
|
|
||||||
|
|
||||||
NGINX has timeout values by default, so if you are running Stirling-PDF behind NGINX, you may need to set a timeout value, such as adding the config `proxy_read_timeout 3600;`.
|
|
||||||
63
SECURITY.md
Normal file
63
SECURITY.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
The Stirling-PDF team takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings.
|
||||||
|
|
||||||
|
### How to Report
|
||||||
|
|
||||||
|
You can report security vulnerabilities through two channels:
|
||||||
|
|
||||||
|
1. **GitHub Security Advisory**:
|
||||||
|
- Navigate to the [Security tab](https://github.com/Stirling-Tools/Stirling-PDF/security) in our repository
|
||||||
|
- Click on "Report a vulnerability"
|
||||||
|
- Provide a detailed description of the vulnerability
|
||||||
|
|
||||||
|
2. **Direct Email**:
|
||||||
|
- Send your report to security@stirlingpdf.com
|
||||||
|
- Please include as much information as possible about the vulnerability
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
When reporting a vulnerability, please provide:
|
||||||
|
|
||||||
|
- A clear description of the vulnerability
|
||||||
|
- Steps to reproduce the issue
|
||||||
|
- Any potential impact
|
||||||
|
- If possible, suggestions for addressing the vulnerability
|
||||||
|
- Your contact information for follow-up questions
|
||||||
|
|
||||||
|
### Response Time
|
||||||
|
|
||||||
|
We aim to acknowledge receipt of your vulnerability report within 48 hours
|
||||||
|
|
||||||
|
### Process
|
||||||
|
|
||||||
|
1. Submit your report through one of the channels above
|
||||||
|
2. Receive an acknowledgment from our team
|
||||||
|
3. Our team will investigate and validate the issue
|
||||||
|
4. We will work on a fix and keep you updated on our progress
|
||||||
|
5. Once resolved, we will publish the fix and acknowledge your contribution (if desired)
|
||||||
|
|
||||||
|
### Bug Bounty
|
||||||
|
|
||||||
|
At this time, we do not offer a bug bounty program. However, we greatly appreciate your efforts in making Stirling-PDF more secure and will acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
Only the latest version of Stirling-PDF is supported for security updates. We do not backport security fixes to older versions.
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Latest | :white_check_mark: |
|
||||||
|
| Older | :x: |
|
||||||
|
|
||||||
|
**Please note:** Before reporting a security issue, ensure you are using the latest version of Stirling-PDF. Security reports for older versions will not be accepted.
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
When deploying Stirling-PDF:
|
||||||
|
|
||||||
|
1. Always use the latest version
|
||||||
|
2. Follow our deployment guidelines
|
||||||
|
3. Regularly check for and apply updates
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
|
|
||||||
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
|
|
||||||
|
|
||||||
| Technology | Ultra-Lite | Full |
|
|
||||||
| ---------- | :--------: | :---: |
|
|
||||||
| Java | ✔️ | ✔️ |
|
|
||||||
| JavaScript | ✔️ | ✔️ |
|
|
||||||
| Libre | | ✔️ |
|
|
||||||
| Python | | ✔️ |
|
|
||||||
| OpenCV | | ✔️ |
|
|
||||||
| qpdf | | ✔️ |
|
|
||||||
|
|
||||||
| Operation | Ultra-Lite | Full |
|
|
||||||
| ---------------------- | ---------- | ---- |
|
|
||||||
| add-page-numbers | ✔️ | ✔️ |
|
|
||||||
| add-password | ✔️ | ✔️ |
|
|
||||||
| add-image | ✔️ | ✔️ |
|
|
||||||
| add-watermark | ✔️ | ✔️ |
|
|
||||||
| adjust-contrast | ✔️ | ✔️ |
|
|
||||||
| auto-split-pdf | ✔️ | ✔️ |
|
|
||||||
| auto-redact | ✔️ | ✔️ |
|
|
||||||
| auto-rename | ✔️ | ✔️ |
|
|
||||||
| cert-sign | ✔️ | ✔️ |
|
|
||||||
| remove-cert-sign | ✔️ | ✔️ |
|
|
||||||
| crop | ✔️ | ✔️ |
|
|
||||||
| change-metadata | ✔️ | ✔️ |
|
|
||||||
| change-permissions | ✔️ | ✔️ |
|
|
||||||
| compare | ✔️ | ✔️ |
|
|
||||||
| extract-page | ✔️ | ✔️ |
|
|
||||||
| extract-images | ✔️ | ✔️ |
|
|
||||||
| flatten | ✔️ | ✔️ |
|
|
||||||
| get-info-on-pdf | ✔️ | ✔️ |
|
|
||||||
| img-to-pdf | ✔️ | ✔️ |
|
|
||||||
| markdown-to-pdf | ✔️ | ✔️ |
|
|
||||||
| merge-pdfs | ✔️ | ✔️ |
|
|
||||||
| multi-page-layout | ✔️ | ✔️ |
|
|
||||||
| overlay-pdf | ✔️ | ✔️ |
|
|
||||||
| pdf-organizer | ✔️ | ✔️ |
|
|
||||||
| pdf-to-csv | ✔️ | ✔️ |
|
|
||||||
| pdf-to-img | ✔️ | ✔️ |
|
|
||||||
| pdf-to-single-page | ✔️ | ✔️ |
|
|
||||||
| remove-pages | ✔️ | ✔️ |
|
|
||||||
| remove-password | ✔️ | ✔️ |
|
|
||||||
| rotate-pdf | ✔️ | ✔️ |
|
|
||||||
| sanitize-pdf | ✔️ | ✔️ |
|
|
||||||
| scale-pages | ✔️ | ✔️ |
|
|
||||||
| sign | ✔️ | ✔️ |
|
|
||||||
| show-javascript | ✔️ | ✔️ |
|
|
||||||
| split-by-size-or-count | ✔️ | ✔️ |
|
|
||||||
| split-pdf-by-sections | ✔️ | ✔️ |
|
|
||||||
| split-pdfs | ✔️ | ✔️ |
|
|
||||||
| compress-pdf | | ✔️ |
|
|
||||||
| extract-image-scans | | ✔️ |
|
|
||||||
| ocr-pdf | | ✔️ |
|
|
||||||
| pdf-to-pdfa | | ✔️ |
|
|
||||||
| remove-blanks | | ✔️ |
|
|
||||||
pdf-to-text | ✔️ | ✔️
|
|
||||||
pdf-to-html | | ✔️
|
|
||||||
pdf-to-word | | ✔️
|
|
||||||
pdf-to-presentation | | ✔️
|
|
||||||
pdf-to-xml | | ✔️
|
|
||||||
remove-annotations | ✔️ | ✔️
|
|
||||||
remove-cert-sign | ✔️ | ✔️
|
|
||||||
remove-image-pdf | ✔️ | ✔️
|
|
||||||
file-to-pdf | | ✔️
|
|
||||||
html-to-pdf | | ✔️
|
|
||||||
url-to-pdf | | ✔️
|
|
||||||
repair | | ✔️
|
|
||||||
12
build.gradle
12
build.gradle
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.4.0"
|
id "org.springframework.boot" version "3.4.1"
|
||||||
id "io.spring.dependency-management" version "1.1.6"
|
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"
|
||||||
id "edu.sc.seis.launch4j" version "3.0.6"
|
id "edu.sc.seis.launch4j" version "3.0.6"
|
||||||
@@ -16,7 +16,7 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.4.0"
|
springBootVersion = "3.4.1"
|
||||||
pdfboxVersion = "3.0.3"
|
pdfboxVersion = "3.0.3"
|
||||||
logbackVersion = "1.5.7"
|
logbackVersion = "1.5.7"
|
||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.12.0"
|
||||||
@@ -27,7 +27,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.36.4"
|
version = "0.36.6"
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -38,10 +38,6 @@ java {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
|
||||||
maven {
|
|
||||||
url 'https://build.shibboleth.net/maven/releases'
|
|
||||||
}
|
|
||||||
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" }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@general
|
@general
|
||||||
Feature: API Validation
|
Feature: API Validation
|
||||||
|
|
||||||
|
|
||||||
@split-pdf-by-sections @positive
|
@split-pdf-by-sections @positive
|
||||||
Scenario Outline: split-pdf-by-sections with different parameters
|
Scenario Outline: split-pdf-by-sections with different parameters
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I generate a PDF file as "fileInput"
|
||||||
@@ -66,7 +66,7 @@ Feature: API Validation
|
|||||||
| pageNumbers | file_count |
|
| pageNumbers | file_count |
|
||||||
| 1,3,5-9 | 8 |
|
| 1,3,5-9 | 8 |
|
||||||
| all | 20 |
|
| all | 20 |
|
||||||
| 2n+1 | 11 |
|
| 2n+1 | 10 |
|
||||||
| 3n | 7 |
|
| 3n | 7 |
|
||||||
|
|
||||||
|
|
||||||
@@ -106,9 +106,9 @@ Feature: API Validation
|
|||||||
And the response ZIP should contain 2 files
|
And the response ZIP should contain 2 files
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
And the response status code should be 200
|
And the response status code should be 200
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
| format |
|
| format |
|
||||||
| png |
|
| png |
|
||||||
| gif |
|
| gif |
|
||||||
| jpeg |
|
| jpeg |
|
||||||
|
|||||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -97,14 +97,14 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
|||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void setCertificateChain(final Certificate[] certificateChain) {
|
|
||||||
this.certificateChain = certificateChain;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Certificate[] getCertificateChain() {
|
public Certificate[] getCertificateChain() {
|
||||||
return certificateChain;
|
return certificateChain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final void setCertificateChain(final Certificate[] certificateChain) {
|
||||||
|
this.certificateChain = certificateChain;
|
||||||
|
}
|
||||||
|
|
||||||
public void setTsaUrl(String tsaUrl) {
|
public void setTsaUrl(String tsaUrl) {
|
||||||
this.tsaUrl = tsaUrl;
|
this.tsaUrl = tsaUrl;
|
||||||
}
|
}
|
||||||
@@ -152,6 +152,10 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isExternalSigning() {
|
||||||
|
return externalSigning;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
|
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
|
||||||
* be used for signing.
|
* be used for signing.
|
||||||
@@ -163,8 +167,4 @@ public abstract class CreateSignatureBase implements SignatureInterface {
|
|||||||
public void setExternalSigning(boolean externalSigning) {
|
public void setExternalSigning(boolean externalSigning) {
|
||||||
this.externalSigning = externalSigning;
|
this.externalSigning = externalSigning;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isExternalSigning() {
|
|
||||||
return externalSigning;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,15 +51,13 @@ public class TSAClient {
|
|||||||
|
|
||||||
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
|
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
|
||||||
new DefaultDigestAlgorithmIdentifierFinder();
|
new DefaultDigestAlgorithmIdentifierFinder();
|
||||||
|
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
|
||||||
|
private static final Random RANDOM = new SecureRandom();
|
||||||
private final URL url;
|
private final URL url;
|
||||||
private final String username;
|
private final String username;
|
||||||
private final String password;
|
private final String password;
|
||||||
private final MessageDigest digest;
|
private final MessageDigest digest;
|
||||||
|
|
||||||
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
|
|
||||||
private static final Random RANDOM = new SecureRandom();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param url the URL of the TSA service
|
* @param url the URL of the TSA service
|
||||||
* @param username user name of TSA
|
* @param username user name of TSA
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.EE;
|
package stirling.software.SPDF.EE;
|
||||||
|
|
||||||
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.Configuration;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
@@ -14,8 +13,15 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class EEAppConfig {
|
public class EEAppConfig {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
@Autowired private LicenseKeyChecker licenseKeyChecker;
|
|
||||||
|
private final LicenseKeyChecker licenseKeyChecker;
|
||||||
|
|
||||||
|
public EEAppConfig(
|
||||||
|
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.licenseKeyChecker = licenseKeyChecker;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "runningEE")
|
@Bean(name = "runningEE")
|
||||||
public boolean runningEnterpriseEdition() {
|
public boolean runningEnterpriseEdition() {
|
||||||
|
|||||||
@@ -17,18 +17,16 @@ public class LibreOfficeListener {
|
|||||||
|
|
||||||
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
|
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
|
||||||
private static final int LISTENER_PORT = 2002;
|
private static final int LISTENER_PORT = 2002;
|
||||||
|
private ExecutorService executorService;
|
||||||
|
private long lastActivityTime;
|
||||||
|
private Process process;
|
||||||
|
|
||||||
|
private LibreOfficeListener() {}
|
||||||
|
|
||||||
public static LibreOfficeListener getInstance() {
|
public static LibreOfficeListener getInstance() {
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ExecutorService executorService;
|
|
||||||
private long lastActivityTime;
|
|
||||||
|
|
||||||
private Process process;
|
|
||||||
|
|
||||||
private LibreOfficeListener() {}
|
|
||||||
|
|
||||||
private boolean isListenerRunning() {
|
private boolean isListenerRunning() {
|
||||||
log.info("waiting for listener to start");
|
log.info("waiting for listener to start");
|
||||||
try (Socket socket = new Socket()) {
|
try (Socket socket = new Socket()) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import java.awt.*;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -11,8 +10,6 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
|
||||||
import javax.swing.*;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
@@ -34,24 +31,22 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SPdfApplication {
|
public class SPdfApplication {
|
||||||
|
|
||||||
@Autowired private Environment env;
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
private static String baseUrlStatic;
|
private static String baseUrlStatic;
|
||||||
private static String serverPortStatic;
|
private static String serverPortStatic;
|
||||||
|
private final Environment env;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final WebBrowser webBrowser;
|
||||||
|
|
||||||
@Value("${baseUrl:http://localhost}")
|
@Value("${baseUrl:http://localhost}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
@Value("${server.port:8080}")
|
public SPdfApplication(
|
||||||
public void setServerPortStatic(String port) {
|
Environment env,
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
ApplicationProperties applicationProperties,
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
@Autowired(required = false) WebBrowser webBrowser) {
|
||||||
SPdfApplication.serverPortStatic =
|
this.env = env;
|
||||||
"0"; // This will let Spring Boot assign an available port
|
this.applicationProperties = applicationProperties;
|
||||||
} else {
|
this.webBrowser = webBrowser;
|
||||||
SPdfApplication.serverPortStatic = port;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
||||||
@@ -72,29 +67,23 @@ public class SPdfApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException, InterruptedException {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
|
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
|
|
||||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
System.setProperty("java.awt.headless", "false");
|
System.setProperty("java.awt.headless", "false");
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setAdditionalProfiles("default");
|
app.setAdditionalProfiles("default");
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
// External config files
|
// External config files
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||||
} else {
|
} else {
|
||||||
log.warn("External configuration file 'configs/settings.yml' does not exist.");
|
log.warn("External configuration file 'configs/settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
@@ -108,21 +97,17 @@ public class SPdfApplication {
|
|||||||
log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
Properties finalProps = new Properties();
|
Properties finalProps = new Properties();
|
||||||
|
|
||||||
if (!propertyFiles.isEmpty()) {
|
if (!propertyFiles.isEmpty()) {
|
||||||
finalProps.putAll(
|
finalProps.putAll(
|
||||||
Collections.singletonMap(
|
Collections.singletonMap(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
propertyFiles.get("spring.config.additional-location")));
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isEmpty()) {
|
if (!props.isEmpty()) {
|
||||||
finalProps.putAll(props);
|
finalProps.putAll(props);
|
||||||
}
|
}
|
||||||
app.setDefaultProperties(finalProps);
|
app.setDefaultProperties(finalProps);
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
// Ensure directories are created
|
// Ensure directories are created
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(Path.of("customFiles/static/"));
|
Files.createDirectories(Path.of("customFiles/static/"));
|
||||||
@@ -130,7 +115,6 @@ public class SPdfApplication {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating directories: {}", e.getMessage());
|
log.error("Error creating directories: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
printStartupLogs();
|
printStartupLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +124,24 @@ public class SPdfApplication {
|
|||||||
log.info("Navigate to {}", url);
|
log.info("Navigate to {}", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired(required = false)
|
public static String getStaticBaseUrl() {
|
||||||
private WebBrowser webBrowser;
|
return baseUrlStatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getStaticPort() {
|
||||||
|
return serverPortStatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${server.port:8080}")
|
||||||
|
public void setServerPortStatic(String port) {
|
||||||
|
if ("auto".equalsIgnoreCase(port)) {
|
||||||
|
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||||
|
SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port
|
||||||
|
"0";
|
||||||
|
} else {
|
||||||
|
SPdfApplication.serverPortStatic = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
@@ -180,18 +180,10 @@ public class SPdfApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getStaticBaseUrl() {
|
|
||||||
return baseUrlStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNonStaticBaseUrl() {
|
public String getNonStaticBaseUrl() {
|
||||||
return baseUrlStatic;
|
return baseUrlStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getStaticPort() {
|
|
||||||
return serverPortStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNonStaticPort() {
|
public String getNonStaticPort() {
|
||||||
return serverPortStatic;
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import java.nio.file.Paths;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -27,7 +26,11 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public AppConfig(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(
|
||||||
@@ -99,6 +102,25 @@ public class AppConfig {
|
|||||||
return Files.exists(Paths.get("/.dockerenv"));
|
return Files.exists(Paths.get("/.dockerenv"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "configDirMounted")
|
||||||
|
public boolean isRunningInDockerWithConfig() {
|
||||||
|
Path dockerEnv = Paths.get("/.dockerenv");
|
||||||
|
// default to true if not docker
|
||||||
|
if (!Files.exists(dockerEnv)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Path mountInfo = Paths.get("/proc/1/mountinfo");
|
||||||
|
// this should always exist, if not some unknown usecase
|
||||||
|
if (!Files.exists(mountInfo)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "bookAndHtmlFormatsInstalled")
|
@Bean(name = "bookAndHtmlFormatsInstalled")
|
||||||
public boolean bookAndHtmlFormatsInstalled() {
|
public boolean bookAndHtmlFormatsInstalled() {
|
||||||
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
||||||
|
|||||||
@@ -11,10 +11,16 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Service
|
@Service
|
||||||
class AppUpdateService {
|
class AppUpdateService {
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired(required = false)
|
private final ShowAdminInterface showAdmin;
|
||||||
ShowAdminInterface showAdmin;
|
|
||||||
|
public AppUpdateService(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
@Autowired(required = false) ShowAdminInterface showAdmin) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.showAdmin = showAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean(name = "shouldShow")
|
@Bean(name = "shouldShow")
|
||||||
@Scope("request")
|
@Scope("request")
|
||||||
|
|||||||
@@ -20,11 +20,10 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
||||||
public class EndpointConfiguration {
|
public class EndpointConfiguration {
|
||||||
|
|
||||||
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
|
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 final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
private boolean bookAndHtmlFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -287,6 +286,4 @@ public class EndpointConfiguration {
|
|||||||
public Set<String> getEndpointsForGroup(String group) {
|
public Set<String> getEndpointsForGroup(String group) {
|
||||||
return endpointGroups.getOrDefault(group, new HashSet<>());
|
return endpointGroups.getOrDefault(group, new HashSet<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
@@ -10,7 +9,11 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
@Component
|
@Component
|
||||||
public class EndpointInterceptor implements HandlerInterceptor {
|
public class EndpointInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
@Autowired private EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
|
||||||
|
public EndpointInterceptor(EndpointConfiguration endpointConfiguration) {
|
||||||
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -15,7 +14,24 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ExternalAppDepConfig {
|
public class ExternalAppDepConfig {
|
||||||
@Autowired private EndpointConfiguration endpointConfiguration;
|
|
||||||
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
private final Map<String, List<String>> commandToGroupMapping =
|
||||||
|
new HashMap<>() {
|
||||||
|
|
||||||
|
{
|
||||||
|
put("soffice", List.of("LibreOffice"));
|
||||||
|
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(EndpointConfiguration endpointConfiguration) {
|
||||||
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCommandAvailable(String command) {
|
private boolean isCommandAvailable(String command) {
|
||||||
try {
|
try {
|
||||||
@@ -34,18 +50,6 @@ public class ExternalAppDepConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Map<String, List<String>> commandToGroupMapping =
|
|
||||||
new HashMap<>() {
|
|
||||||
{
|
|
||||||
put("soffice", List.of("LibreOffice"));
|
|
||||||
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"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private List<String> getAffectedFeatures(String group) {
|
private List<String> getAffectedFeatures(String group) {
|
||||||
return endpointConfiguration.getEndpointsForGroup(group).stream()
|
return endpointConfiguration.getEndpointsForGroup(group).stream()
|
||||||
.map(endpoint -> formatEndpointAsFeature(endpoint))
|
.map(endpoint -> formatEndpointAsFeature(endpoint))
|
||||||
@@ -55,7 +59,6 @@ public class ExternalAppDepConfig {
|
|||||||
private String formatEndpointAsFeature(String endpoint) {
|
private String formatEndpointAsFeature(String endpoint) {
|
||||||
// First replace common terms
|
// First replace common terms
|
||||||
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
|
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
|
||||||
|
|
||||||
// Split into words and capitalize each word
|
// Split into words and capitalize each word
|
||||||
return Arrays.stream(feature.split("\\s+"))
|
return Arrays.stream(feature.split("\\s+"))
|
||||||
.map(word -> capitalizeWord(word))
|
.map(word -> capitalizeWord(word))
|
||||||
@@ -76,7 +79,6 @@ public class ExternalAppDepConfig {
|
|||||||
boolean isAvailable = isCommandAvailable(command);
|
boolean isAvailable = isCommandAvailable(command);
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
List<String> affectedGroups = commandToGroupMapping.get(command);
|
List<String> affectedGroups = commandToGroupMapping.get(command);
|
||||||
|
|
||||||
if (affectedGroups != null) {
|
if (affectedGroups != null) {
|
||||||
for (String group : affectedGroups) {
|
for (String group : affectedGroups) {
|
||||||
List<String> affectedFeatures = getAffectedFeatures(group);
|
List<String> affectedFeatures = getAffectedFeatures(group);
|
||||||
@@ -95,7 +97,6 @@ public class ExternalAppDepConfig {
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void checkDependencies() {
|
public void checkDependencies() {
|
||||||
|
|
||||||
// Check core dependencies
|
// Check core dependencies
|
||||||
checkDependencyAndDisableGroup("tesseract");
|
checkDependencyAndDisableGroup("tesseract");
|
||||||
checkDependencyAndDisableGroup("soffice");
|
checkDependencyAndDisableGroup("soffice");
|
||||||
@@ -103,13 +104,11 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup("weasyprint");
|
checkDependencyAndDisableGroup("weasyprint");
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup("unoconv");
|
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) {
|
||||||
List<String> pythonFeatures = getAffectedFeatures("Python");
|
List<String> pythonFeatures = getAffectedFeatures("Python");
|
||||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
||||||
|
|
||||||
endpointConfiguration.disableGroup("Python");
|
endpointConfiguration.disableGroup("Python");
|
||||||
endpointConfiguration.disableGroup("OpenCV");
|
endpointConfiguration.disableGroup("OpenCV");
|
||||||
log.warn(
|
log.warn(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
@@ -23,25 +22,26 @@ import stirling.software.SPDF.utils.GeneralUtils;
|
|||||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||||
public class InitialSetup {
|
public class InitialSetup {
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public InitialSetup(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() throws IOException {
|
public void init() throws IOException {
|
||||||
initUUIDKey();
|
initUUIDKey();
|
||||||
|
|
||||||
initSecretKey();
|
initSecretKey();
|
||||||
|
|
||||||
initEnableCSRFSecurity();
|
initEnableCSRFSecurity();
|
||||||
|
|
||||||
initLegalUrls();
|
initLegalUrls();
|
||||||
|
|
||||||
initSetAppVersion();
|
initSetAppVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initUUIDKey() throws IOException {
|
public void initUUIDKey() throws IOException {
|
||||||
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||||
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
|
uuid = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
||||||
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,8 @@ public class InitialSetup {
|
|||||||
public void initSecretKey() throws IOException {
|
public void initSecretKey() throws IOException {
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
if (!GeneralUtils.isValidUUID(secretKey)) {
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
|
secretKey = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
||||||
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,6 @@ public class InitialSetup {
|
|||||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
|
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)) {
|
||||||
@@ -87,7 +87,6 @@ public class InitialSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void initSetAppVersion() throws IOException {
|
public void initSetAppVersion() throws IOException {
|
||||||
|
|
||||||
String appVersion = "0.0.0";
|
String appVersion = "0.0.0";
|
||||||
Resource resource = new ClassPathResource("version.properties");
|
Resource resource = new ClassPathResource("version.properties");
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
@@ -95,7 +94,6 @@ public class InitialSetup {
|
|||||||
props.load(resource.getInputStream());
|
props.load(resource.getInputStream());
|
||||||
appVersion = props.getProperty("version");
|
appVersion = props.getProperty("version");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
}
|
}
|
||||||
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package stirling.software.SPDF.config;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
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.Configuration;
|
||||||
import org.springframework.web.servlet.LocaleResolver;
|
import org.springframework.web.servlet.LocaleResolver;
|
||||||
@@ -16,7 +15,11 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class LocaleConfiguration implements WebMvcConfigurer {
|
public class LocaleConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public LocaleConfiguration(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
@@ -34,21 +37,17 @@ public class LocaleConfiguration implements WebMvcConfigurer {
|
|||||||
@Bean
|
@Bean
|
||||||
public LocaleResolver localeResolver() {
|
public LocaleResolver localeResolver() {
|
||||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
|
|
||||||
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||||
Locale defaultLocale =
|
Locale defaultLocale = // Fallback to UK locale if environment variable is not set
|
||||||
Locale.UK; // Fallback to UK locale if environment variable is not set
|
Locale.UK;
|
||||||
|
|
||||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
|
|
||||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
defaultLocale = tempLocale;
|
defaultLocale = tempLocale;
|
||||||
} else {
|
} else {
|
||||||
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
|
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
|
||||||
tempLanguageTag = tempLocale.toLanguageTag();
|
tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
|
|
||||||
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
|
||||||
defaultLocale = tempLocale;
|
defaultLocale = tempLocale;
|
||||||
} else {
|
} else {
|
||||||
@@ -57,7 +56,6 @@ public class LocaleConfiguration implements WebMvcConfigurer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slr.setDefaultLocale(defaultLocale);
|
slr.setDefaultLocale(defaultLocale);
|
||||||
return slr;
|
return slr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
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.Configuration;
|
||||||
|
|
||||||
@@ -15,15 +14,19 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public OpenApiConfig(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenAPI customOpenAPI() {
|
public OpenAPI customOpenAPI() {
|
||||||
String version = getClass().getPackage().getImplementationVersion();
|
String version = getClass().getPackage().getImplementationVersion();
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
version = "1.0.0"; // default version if all else fails
|
// default version if all else fails
|
||||||
|
version = "1.0.0";
|
||||||
}
|
}
|
||||||
|
|
||||||
SecurityScheme apiKeyScheme =
|
SecurityScheme apiKeyScheme =
|
||||||
new SecurityScheme()
|
new SecurityScheme()
|
||||||
.type(SecurityScheme.Type.APIKEY)
|
.type(SecurityScheme.Type.APIKEY)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
@@ -9,7 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class WebMvcConfig implements WebMvcConfigurer {
|
public class WebMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired private EndpointInterceptor endpointInterceptor;
|
private final EndpointInterceptor endpointInterceptor;
|
||||||
|
|
||||||
|
public WebMvcConfig(EndpointInterceptor endpointInterceptor) {
|
||||||
|
this.endpointInterceptor = endpointInterceptor;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.List;
|
|||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
public interface DatabaseBackupInterface {
|
public interface DatabaseBackupInterface {
|
||||||
|
|
||||||
void exportDatabase() throws IOException;
|
void exportDatabase() throws IOException;
|
||||||
|
|
||||||
boolean importDatabase();
|
boolean importDatabase();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -15,8 +14,15 @@ import stirling.software.SPDF.repository.UserRepository;
|
|||||||
@Service
|
@Service
|
||||||
class AppUpdateAuthService implements ShowAdminInterface {
|
class AppUpdateAuthService implements ShowAdminInterface {
|
||||||
|
|
||||||
@Autowired private UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public AppUpdateAuthService(
|
||||||
|
UserRepository userRepository, ApplicationProperties applicationProperties) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean getShowUpdateOnlyAdmins() {
|
public boolean getShowUpdateOnlyAdmins() {
|
||||||
@@ -24,24 +30,18 @@ class AppUpdateAuthService implements ShowAdminInterface {
|
|||||||
if (!showUpdate) {
|
if (!showUpdate) {
|
||||||
return showUpdate;
|
return showUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
|
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
|
||||||
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return !showUpdateOnlyAdmin;
|
return !showUpdateOnlyAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
|
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
|
||||||
return !showUpdateOnlyAdmin;
|
return !showUpdateOnlyAdmin;
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> user = userRepository.findByUsername(authentication.getName());
|
Optional<User> user = userRepository.findByUsername(authentication.getName());
|
||||||
if (user.isPresent() && showUpdateOnlyAdmin) {
|
if (user.isPresent() && showUpdateOnlyAdmin) {
|
||||||
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
|
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return showUpdate;
|
return showUpdate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,9 +219,9 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
// + response.encodeRedirectURL(redirect_url);
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
log.info("Google does not have a specific logout URL");
|
log.info("Google does not have a specific logout URL");
|
||||||
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
// break;
|
// break;
|
||||||
default:
|
default:
|
||||||
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.util.Collection;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
@@ -20,9 +19,15 @@ import stirling.software.SPDF.repository.UserRepository;
|
|||||||
@Service
|
@Service
|
||||||
public class CustomUserDetailsService implements UserDetailsService {
|
public class CustomUserDetailsService implements UserDetailsService {
|
||||||
|
|
||||||
@Autowired private UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
public CustomUserDetailsService(
|
||||||
|
UserRepository userRepository, LoginAttemptService loginAttemptService) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
@@ -33,16 +38,13 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
() ->
|
() ->
|
||||||
new UsernameNotFoundException(
|
new UsernameNotFoundException(
|
||||||
"No user found with username: " + username));
|
"No user found with username: " + username));
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
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 (!user.hasPassword()) {
|
if (!user.hasPassword()) {
|
||||||
throw new IllegalArgumentException("Password must not be null");
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import java.text.SimpleDateFormat;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
@@ -25,7 +24,11 @@ import stirling.software.SPDF.utils.RequestUriUtils;
|
|||||||
@Component
|
@Component
|
||||||
public class FirstLoginFilter extends OncePerRequestFilter {
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
@Lazy private final UserService userService;
|
||||||
|
|
||||||
|
public FirstLoginFilter(@Lazy UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@@ -34,16 +37,13 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
|
|
||||||
// Check if the request is for static resources
|
// Check if the request is for static resources
|
||||||
boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI);
|
boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI);
|
||||||
|
|
||||||
// If it's a static resource, just continue the filter chain and skip the logic below
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
if (isStaticResource) {
|
if (isStaticResource) {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName());
|
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName());
|
||||||
@@ -55,12 +55,10 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
HttpSession session = request.getSession(true);
|
HttpSession session = request.getSession(true);
|
||||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
|
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
|
||||||
String creationTime = timeFormat.format(new Date(session.getCreationTime()));
|
String creationTime = timeFormat.format(new Date(session.getCreationTime()));
|
||||||
|
|
||||||
log.debug(
|
log.debug(
|
||||||
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
|
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
|
||||||
session.isNew(),
|
session.isNew(),
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ import java.io.IOException;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import jakarta.servlet.Filter;
|
import jakarta.servlet.*;
|
||||||
import jakarta.servlet.FilterChain;
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.ServletRequest;
|
|
||||||
import jakarta.servlet.ServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -16,11 +15,20 @@ import stirling.software.SPDF.model.Role;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class InitialSecuritySetup {
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired private DatabaseBackupInterface databaseBackupHelper;
|
private final DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
|
public InitialSecuritySetup(
|
||||||
|
UserService userService,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
DatabaseBackupInterface databaseBackupHelper) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.databaseBackupHelper = databaseBackupHelper;
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() throws IllegalArgumentException, IOException {
|
public void init() throws IllegalArgumentException, IOException {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@@ -15,13 +14,20 @@ import stirling.software.SPDF.model.AttemptCounter;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LoginAttemptService {
|
public class LoginAttemptService {
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private int MAX_ATTEMPT;
|
private int MAX_ATTEMPT;
|
||||||
|
|
||||||
private long ATTEMPT_INCREMENT_TIME;
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
|
|
||||||
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
||||||
|
|
||||||
private boolean isBlockedEnabled = true;
|
private boolean isBlockedEnabled = true;
|
||||||
|
|
||||||
|
public LoginAttemptService(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
@@ -46,7 +52,6 @@ public class LoginAttemptService {
|
|||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
attemptCounter = new AttemptCounter();
|
attemptCounter = new AttemptCounter();
|
||||||
@@ -67,20 +72,18 @@ public class LoginAttemptService {
|
|||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
|
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRemainingAttempts(String key) {
|
public int getRemainingAttempts(String key) {
|
||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled
|
// Arbitrarily high number if tracking is disabled
|
||||||
|
return Integer.MAX_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
return MAX_ATTEMPT;
|
return MAX_ATTEMPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MAX_ATTEMPT - attemptCounter.getAttemptCount();
|
return MAX_ATTEMPT - attemptCounter.getAttemptCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.security.cert.X509Certificate;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@@ -63,6 +62,7 @@ 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.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
import stirling.software.SPDF.repository.PersistentLoginRepository;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@@ -71,38 +71,64 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|||||||
@DependsOn("runningEE")
|
@DependsOn("runningEE")
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
@Lazy private final UserService userService;
|
||||||
|
|
||||||
|
@Qualifier("loginEnabled")
|
||||||
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Qualifier("runningEE")
|
||||||
|
private final boolean runningEE;
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final UserAuthenticationFilter userAuthenticationFilter;
|
||||||
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
private final FirstLoginFilter firstLoginFilter;
|
||||||
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
|
private final PersistentLoginRepository persistentLoginRepository;
|
||||||
|
|
||||||
|
// // Only Dev test
|
||||||
|
// @Bean
|
||||||
|
// public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
|
// return (web) ->
|
||||||
|
// web.ignoring()
|
||||||
|
// .requestMatchers(
|
||||||
|
// "/css/**", "/images/**", "/js/**", "/**.svg",
|
||||||
|
// "/pdfjs-legacy/**");
|
||||||
|
// }
|
||||||
|
public SecurityConfiguration(
|
||||||
|
PersistentLoginRepository persistentLoginRepository,
|
||||||
|
CustomUserDetailsService userDetailsService,
|
||||||
|
@Lazy UserService userService,
|
||||||
|
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||||
|
@Qualifier("runningEE") boolean runningEE,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserAuthenticationFilter userAuthenticationFilter,
|
||||||
|
LoginAttemptService loginAttemptService,
|
||||||
|
FirstLoginFilter firstLoginFilter,
|
||||||
|
SessionPersistentRegistry sessionRegistry) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
|
this.runningEE = runningEE;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userAuthenticationFilter = userAuthenticationFilter;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.firstLoginFilter = firstLoginFilter;
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.persistentLoginRepository = persistentLoginRepository;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("loginEnabled")
|
|
||||||
public boolean loginEnabledValue;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("runningEE")
|
|
||||||
public boolean runningEE;
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
|
||||||
|
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
|
||||||
|
|
||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
|
||||||
@Autowired private SessionPersistentRegistry sessionRegistry;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
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);
|
||||||
@@ -117,13 +143,11 @@ public class SecurityConfiguration {
|
|||||||
csrf.ignoringRequestMatchers(
|
csrf.ignoringRequestMatchers(
|
||||||
request -> {
|
request -> {
|
||||||
String apiKey = request.getHeader("X-API-KEY");
|
String apiKey = request.getHeader("X-API-KEY");
|
||||||
|
|
||||||
// If there's no API key, don't ignore CSRF
|
// If there's no API key, don't ignore CSRF
|
||||||
// (return false)
|
// (return false)
|
||||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
if (apiKey == null || apiKey.trim().isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate API key using existing UserService
|
// Validate API key using existing UserService
|
||||||
try {
|
try {
|
||||||
Optional<User> user =
|
Optional<User> user =
|
||||||
@@ -152,7 +176,6 @@ public class SecurityConfiguration {
|
|||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.authenticationProvider(daoAuthenticationProvider());
|
http.authenticationProvider(daoAuthenticationProvider());
|
||||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
||||||
http.logout(
|
http.logout(
|
||||||
@@ -161,18 +184,23 @@ public class SecurityConfiguration {
|
|||||||
.logoutSuccessHandler(
|
.logoutSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(applicationProperties))
|
new CustomLogoutSuccessHandler(applicationProperties))
|
||||||
.clearAuthentication(true)
|
.clearAuthentication(true)
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
.invalidateHttpSession( // Invalidate session
|
||||||
|
true)
|
||||||
.deleteCookies("JSESSIONID", "remember-me"));
|
.deleteCookies("JSESSIONID", "remember-me"));
|
||||||
http.rememberMe(
|
http.rememberMe(
|
||||||
rememberMeConfigurer ->
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
rememberMeConfigurer // Use the configurator directly
|
rememberMeConfigurer
|
||||||
.tokenRepository(persistentTokenRepository())
|
.tokenRepository(persistentTokenRepository())
|
||||||
.tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days
|
.tokenValiditySeconds( // 14 days
|
||||||
.userDetailsService(
|
14 * 24 * 60 * 60)
|
||||||
userDetailsService) // Your existing UserDetailsService
|
.userDetailsService( // Your existing UserDetailsService
|
||||||
.useSecureCookie(true) // Enable secure cookie
|
userDetailsService)
|
||||||
.rememberMeParameter("remember-me") // Form parameter name
|
.useSecureCookie( // Enable secure cookie
|
||||||
.rememberMeCookieName("remember-me") // Cookie name
|
true)
|
||||||
|
.rememberMeParameter( // Form parameter name
|
||||||
|
"remember-me")
|
||||||
|
.rememberMeCookieName( // Cookie name
|
||||||
|
"remember-me")
|
||||||
.alwaysRemember(false));
|
.alwaysRemember(false));
|
||||||
http.authorizeHttpRequests(
|
http.authorizeHttpRequests(
|
||||||
authz ->
|
authz ->
|
||||||
@@ -180,14 +208,12 @@ public class SecurityConfiguration {
|
|||||||
req -> {
|
req -> {
|
||||||
String uri = req.getRequestURI();
|
String uri = req.getRequestURI();
|
||||||
String contextPath = req.getContextPath();
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
// Remove the context path from the URI
|
// Remove the context path from the URI
|
||||||
String trimmedUri =
|
String trimmedUri =
|
||||||
uri.startsWith(contextPath)
|
uri.startsWith(contextPath)
|
||||||
? uri.substring(
|
? uri.substring(
|
||||||
contextPath.length())
|
contextPath.length())
|
||||||
: uri;
|
: uri;
|
||||||
|
|
||||||
return trimmedUri.startsWith("/login")
|
return trimmedUri.startsWith("/login")
|
||||||
|| trimmedUri.startsWith("/oauth")
|
|| trimmedUri.startsWith("/oauth")
|
||||||
|| trimmedUri.startsWith("/saml2")
|
|| trimmedUri.startsWith("/saml2")
|
||||||
@@ -205,7 +231,6 @@ public class SecurityConfiguration {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated());
|
.authenticated());
|
||||||
|
|
||||||
// Handle User/Password Logins
|
// Handle User/Password Logins
|
||||||
if (applicationProperties.getSecurity().isUserPass()) {
|
if (applicationProperties.getSecurity().isUserPass()) {
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
@@ -221,27 +246,26 @@ public class SecurityConfiguration {
|
|||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().isOauth2Activ()) {
|
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 autocreated 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(
|
||||||
new CustomOAuth2AuthenticationSuccessHandler(
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
applicationProperties,
|
applicationProperties,
|
||||||
userService))
|
userService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomOAuth2AuthenticationFailureHandler())
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
// Add existing Authorities from the database
|
. // Add existing Authorities from the database
|
||||||
.userInfoEndpoint(
|
userInfoEndpoint(
|
||||||
userInfoEndpoint ->
|
userInfoEndpoint ->
|
||||||
userInfoEndpoint
|
userInfoEndpoint
|
||||||
.oidcUserService(
|
.oidcUserService(
|
||||||
@@ -253,15 +277,14 @@ public class SecurityConfiguration {
|
|||||||
userAuthoritiesMapper()))
|
userAuthoritiesMapper()))
|
||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
|
if (applicationProperties.getSecurity().isSaml2Activ()) {
|
||||||
|
// && runningEE
|
||||||
// Configure the authentication provider
|
// Configure the authentication provider
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
new OpenSaml4AuthenticationProvider();
|
new OpenSaml4AuthenticationProvider();
|
||||||
authenticationProvider.setResponseAuthenticationConverter(
|
authenticationProvider.setResponseAuthenticationConverter(
|
||||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||||
|
|
||||||
http.authenticationProvider(authenticationProvider)
|
http.authenticationProvider(authenticationProvider)
|
||||||
.saml2Login(
|
.saml2Login(
|
||||||
saml2 -> {
|
saml2 -> {
|
||||||
@@ -287,7 +310,6 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||||
// CookieCsrfTokenRepository cookieRepo =
|
// CookieCsrfTokenRepository cookieRepo =
|
||||||
@@ -302,7 +324,6 @@ public class SecurityConfiguration {
|
|||||||
// }
|
// }
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,17 +334,14 @@ public class SecurityConfiguration {
|
|||||||
matchIfMissing = false)
|
matchIfMissing = false)
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
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("At least one OAuth2 provider must be configured");
|
log.error("At least one OAuth2 provider must be configured");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InMemoryClientRegistrationRepository(registrations);
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +384,6 @@ public class SecurityConfiguration {
|
|||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
|
|
||||||
return keycloak != null && keycloak.isSettingsValid()
|
return keycloak != null && keycloak.isSettingsValid()
|
||||||
? Optional.of(
|
? Optional.of(
|
||||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||||
@@ -381,7 +398,6 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -443,19 +459,15 @@ public class SecurityConfiguration {
|
|||||||
matchIfMissing = false)
|
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();
|
||||||
|
|
||||||
Saml2X509Credential signingCredential =
|
Saml2X509Credential signingCredential =
|
||||||
new Saml2X509Credential(
|
new Saml2X509Credential(
|
||||||
CertificateUtils.readPrivateKey(privateKeyResource),
|
CertificateUtils.readPrivateKey(privateKeyResource),
|
||||||
CertificateUtils.readCertificate(certificateResource),
|
CertificateUtils.readCertificate(certificateResource),
|
||||||
Saml2X509CredentialType.SIGNING);
|
Saml2X509CredentialType.SIGNING);
|
||||||
|
|
||||||
RelyingPartyRegistration rp =
|
RelyingPartyRegistration rp =
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||||
.signingX509Credentials(c -> c.add(signingCredential))
|
.signingX509Credentials(c -> c.add(signingCredential))
|
||||||
@@ -470,7 +482,6 @@ public class SecurityConfiguration {
|
|||||||
Saml2MessageBinding.POST)
|
Saml2MessageBinding.POST)
|
||||||
.wantAuthnRequestsSigned(true))
|
.wantAuthnRequestsSigned(true))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,10 +497,8 @@ public class SecurityConfiguration {
|
|||||||
resolver.setAuthnRequestCustomizer(
|
resolver.setAuthnRequestCustomizer(
|
||||||
customizer -> {
|
customizer -> {
|
||||||
log.debug("Customizing SAML Authentication request");
|
log.debug("Customizing SAML Authentication request");
|
||||||
|
|
||||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||||
|
|
||||||
if (authnRequest.getID() == null) {
|
if (authnRequest.getID() == null) {
|
||||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||||
}
|
}
|
||||||
@@ -500,16 +509,13 @@ public class SecurityConfiguration {
|
|||||||
authnRequest.getIssuer() != null
|
authnRequest.getIssuer() != null
|
||||||
? authnRequest.getIssuer().getValue()
|
? authnRequest.getIssuer().getValue()
|
||||||
: "null");
|
: "null");
|
||||||
|
|
||||||
HttpServletRequest request = customizer.getRequest();
|
HttpServletRequest request = customizer.getRequest();
|
||||||
|
|
||||||
// Log HTTP request details
|
// Log HTTP request details
|
||||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||||
log.debug("Request URI: {}", request.getRequestURI());
|
log.debug("Request URI: {}", request.getRequestURI());
|
||||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||||
log.debug("Query String: {}", request.getQueryString());
|
log.debug("Query String: {}", request.getQueryString());
|
||||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||||
|
|
||||||
// Log headers
|
// Log headers
|
||||||
Collections.list(request.getHeaderNames())
|
Collections.list(request.getHeaderNames())
|
||||||
.forEach(
|
.forEach(
|
||||||
@@ -519,24 +525,20 @@ public class SecurityConfiguration {
|
|||||||
headerName,
|
headerName,
|
||||||
request.getHeader(headerName));
|
request.getHeader(headerName));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log SAML specific parameters
|
// Log SAML specific parameters
|
||||||
log.debug("SAML Request Parameters:");
|
log.debug("SAML Request Parameters:");
|
||||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||||
|
|
||||||
// Log session debugrmation if exists
|
// Log session debugrmation if exists
|
||||||
if (request.getSession(false) != null) {
|
if (request.getSession(false) != null) {
|
||||||
log.debug("Session ID: {}", request.getSession().getId());
|
log.debug("Session ID: {}", request.getSession().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log any assertions consumer service details if present
|
// Log any assertions consumer service details if present
|
||||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"AssertionConsumerServiceURL: {}",
|
"AssertionConsumerServiceURL: {}",
|
||||||
authnRequest.getAssertionConsumerServiceURL());
|
authnRequest.getAssertionConsumerServiceURL());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log NameID policy if present
|
// Log NameID policy if present
|
||||||
if (authnRequest.getNameIDPolicy() != null) {
|
if (authnRequest.getNameIDPolicy() != null) {
|
||||||
log.debug(
|
log.debug(
|
||||||
@@ -566,12 +568,10 @@ public class SecurityConfiguration {
|
|||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
return (authorities) -> {
|
return (authorities) -> {
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
authorities.forEach(
|
authorities.forEach(
|
||||||
authority -> {
|
authority -> {
|
||||||
// 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 =
|
||||||
@@ -598,27 +598,18 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IPRateLimitingFilter rateLimitingFilter() {
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
// Example limit TODO add config level
|
||||||
|
int maxRequestsPerIp = 1000000;
|
||||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl();
|
return new JPATokenRepositoryImpl(persistentLoginRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public boolean activSecurity() {
|
public boolean activSecurity() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Only Dev test
|
|
||||||
// @Bean
|
|
||||||
// public WebSecurityCustomizer webSecurityCustomizer() {
|
|
||||||
// return (web) ->
|
|
||||||
// web.ignoring()
|
|
||||||
// .requestMatchers(
|
|
||||||
// "/css/**", "/images/**", "/js/**", "/**.svg",
|
|
||||||
// "/pdfjs-legacy/**");
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,22 +22,31 @@ 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 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;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
public UserAuthenticationFilter(
|
public UserAuthenticationFilter(
|
||||||
|
@Lazy ApplicationProperties applicationProperties,
|
||||||
@Lazy UserService userService,
|
@Lazy UserService userService,
|
||||||
SessionPersistentRegistry sessionPersistentRegistry,
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
@@ -121,33 +130,67 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// Check if the authenticated user is disabled and invalidate their session if so
|
// Check if the authenticated user is disabled and invalidate their session if so
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
|
||||||
|
Security securityProp = applicationProperties.getSecurity();
|
||||||
|
LoginMethod loginMethod = LoginMethod.UNKNOWN;
|
||||||
|
|
||||||
|
boolean blockRegistration = false;
|
||||||
|
|
||||||
|
// 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) {
|
if (principal instanceof UserDetails) {
|
||||||
username = ((UserDetails) principal).getUsername();
|
username = ((UserDetails) principal).getUsername();
|
||||||
|
loginMethod = LoginMethod.USERDETAILS;
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
username = ((OAuth2User) principal).getName();
|
username = ((OAuth2User) principal).getName();
|
||||||
|
loginMethod = LoginMethod.OAUTH2USER;
|
||||||
|
OAUTH2 oAuth = securityProp.getOauth2();
|
||||||
|
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
|
loginMethod = LoginMethod.SAML2USER;
|
||||||
|
SAML2 saml2 = securityProp.getSaml2();
|
||||||
|
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
username = (String) principal;
|
username = (String) principal;
|
||||||
|
loginMethod = LoginMethod.STRINGUSER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve all active sessions for the user
|
||||||
List<SessionInformation> sessionsInformations =
|
List<SessionInformation> sessionsInformations =
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
|
||||||
|
// Check if the user exists, is disabled, or needs session invalidation
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
|
log.debug("Validating user: {}", username);
|
||||||
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
||||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
|
boolean notSsoLogin =
|
||||||
|
!loginMethod.equals(LoginMethod.OAUTH2USER)
|
||||||
|
&& !loginMethod.equals(LoginMethod.SAML2USER);
|
||||||
|
|
||||||
|
// Block user registration if not allowed by configuration
|
||||||
|
if (blockRegistration && !isUserExists) {
|
||||||
|
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire sessions and logout if the user does not exist or is disabled
|
||||||
if (!isUserExists || isUserDisabled) {
|
if (!isUserExists || isUserDisabled) {
|
||||||
|
log.info(
|
||||||
|
"Invalidating session for disabled or non-existent user: {}", username);
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
sessionsInformation.expireNow();
|
sessionsInformation.expireNow();
|
||||||
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUserExists) {
|
// Redirect to logout if credentials are invalid
|
||||||
|
if (!isUserExists && notSsoLogin) {
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,6 +204,25 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum LoginMethod {
|
||||||
|
USERDETAILS("UserDetails"),
|
||||||
|
OAUTH2USER("OAuth2User"),
|
||||||
|
STRINGUSER("StringUser"),
|
||||||
|
UNKNOWN("Unknown"),
|
||||||
|
SAML2USER("Saml2User");
|
||||||
|
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
LoginMethod(String method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import java.time.Duration;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
@@ -31,13 +29,15 @@ import stirling.software.SPDF.model.Role;
|
|||||||
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Autowired private UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("rateLimit")
|
@Qualifier("rateLimit")
|
||||||
public boolean rateLimit;
|
private final boolean rateLimit;
|
||||||
|
|
||||||
|
public UserBasedRateLimitingFilter(@Qualifier("rateLimit") boolean rateLimit) {
|
||||||
|
this.rateLimit = rateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@@ -48,21 +48,18 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
if (!"POST".equalsIgnoreCase(method)) {
|
if (!"POST".equalsIgnoreCase(method)) {
|
||||||
// If the request is not a POST, just pass it through without rate limiting
|
// If the request is not a POST, just pass it through without rate limiting
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String identifier = null;
|
String identifier = null;
|
||||||
|
|
||||||
// Check for API key in the request headers
|
// Check for API key in the request headers
|
||||||
String apiKey = request.getHeader("X-API-KEY");
|
String apiKey = request.getHeader("X-API-KEY");
|
||||||
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
identifier =
|
identifier = // Prefix to distinguish between API keys and usernames
|
||||||
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
"API_KEY_" + apiKey;
|
||||||
} else {
|
} else {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
@@ -70,15 +67,12 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
identifier = userDetails.getUsername();
|
identifier = userDetails.getUsername();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If neither API key nor an authenticated user is present, use IP address
|
// If neither API key nor an authenticated user is present, use IP address
|
||||||
if (identifier == null) {
|
if (identifier == null) {
|
||||||
identifier = request.getRemoteAddr();
|
identifier = request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
|
|
||||||
Role userRole =
|
Role userRole =
|
||||||
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
|
|
||||||
if (request.getHeader("X-API-KEY") != null) {
|
if (request.getHeader("X-API-KEY") != null) {
|
||||||
// It's an API call
|
// It's an API call
|
||||||
processRequest(
|
processRequest(
|
||||||
@@ -123,7 +117,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
|
||||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||||
|
|
||||||
if (probe.isConsumed()) {
|
if (probe.isConsumed()) {
|
||||||
response.setHeader(
|
response.setHeader(
|
||||||
"X-Rate-Limit-Remaining",
|
"X-Rate-Limit-Remaining",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.MessageSource;
|
import org.springframework.context.MessageSource;
|
||||||
import org.springframework.context.i18n.LocaleContextHolder;
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
@@ -25,11 +24,7 @@ import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
|||||||
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.ApplicationProperties;
|
import stirling.software.SPDF.model.*;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
|
||||||
import stirling.software.SPDF.model.Authority;
|
|
||||||
import stirling.software.SPDF.model.Role;
|
|
||||||
import stirling.software.SPDF.model.User;
|
|
||||||
import stirling.software.SPDF.repository.AuthorityRepository;
|
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@@ -37,19 +32,36 @@ import stirling.software.SPDF.repository.UserRepository;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class UserService implements UserServiceInterface {
|
public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
@Autowired private UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
@Autowired private AuthorityRepository authorityRepository;
|
private final AuthorityRepository authorityRepository;
|
||||||
|
|
||||||
@Autowired private PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Autowired private MessageSource messageSource;
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionRegistry;
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
private final DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public UserService(
|
||||||
|
UserRepository userRepository,
|
||||||
|
AuthorityRepository authorityRepository,
|
||||||
|
PasswordEncoder passwordEncoder,
|
||||||
|
MessageSource messageSource,
|
||||||
|
SessionPersistentRegistry sessionRegistry,
|
||||||
|
DatabaseBackupInterface databaseBackupHelper,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.authorityRepository = authorityRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.messageSource = messageSource;
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.databaseBackupHelper = databaseBackupHelper;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void migrateOauth2ToSSO() {
|
public void migrateOauth2ToSSO() {
|
||||||
@@ -84,13 +96,11 @@ public class UserService implements UserServiceInterface {
|
|||||||
if (!user.isPresent()) {
|
if (!user.isPresent()) {
|
||||||
throw new UsernameNotFoundException("API key is not valid");
|
throw new UsernameNotFoundException("API key is not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the user into an Authentication object
|
// Convert the user into an Authentication object
|
||||||
return new UsernamePasswordAuthenticationToken(
|
return new UsernamePasswordAuthenticationToken( // principal (typically the user)
|
||||||
user, // principal (typically the user)
|
user, // credentials (we don't expose the password or API key here)
|
||||||
null, // credentials (we don't expose the password or API key here)
|
null, // user's authorities (roles/permissions)
|
||||||
getAuthorities(user.get()) // user's authorities (roles/permissions)
|
getAuthorities(user.get()));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||||
@@ -104,7 +114,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
apiKey = UUID.randomUUID().toString();
|
apiKey = UUID.randomUUID().toString();
|
||||||
} while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
|
} while ( // Ensure uniqueness
|
||||||
|
userRepository.findByApiKey(apiKey).isPresent());
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +129,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
return addApiKeyToUser(username); // reuse the add API key method for refreshing
|
// reuse the add API key method for refreshing
|
||||||
|
return addApiKeyToUser(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
@@ -138,11 +150,11 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public Optional<User> loadUserByApiKey(String apiKey) {
|
public Optional<User> loadUserByApiKey(String apiKey) {
|
||||||
Optional<User> user = userRepository.findByApiKey(apiKey);
|
Optional<User> user = userRepository.findByApiKey(apiKey);
|
||||||
|
|
||||||
if (user.isPresent()) {
|
if (user.isPresent()) {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
return null; // or throw an exception
|
// or throw an exception
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
@@ -240,14 +252,12 @@ public class UserService implements UserServiceInterface {
|
|||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
Map<String, String> settingsMap = user.getSettings();
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
|
|
||||||
if (settingsMap == null) {
|
if (settingsMap == null) {
|
||||||
settingsMap = new HashMap<>();
|
settingsMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
settingsMap.clear();
|
settingsMap.clear();
|
||||||
settingsMap.putAll(updates);
|
settingsMap.putAll(updates);
|
||||||
user.setSettings(settingsMap);
|
user.setSettings(settingsMap);
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
@@ -316,12 +326,9 @@ public class UserService implements UserServiceInterface {
|
|||||||
boolean isValidEmail =
|
boolean isValidEmail =
|
||||||
username.matches(
|
username.matches(
|
||||||
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
||||||
|
|
||||||
List<String> notAllowedUserList = new ArrayList<>();
|
List<String> notAllowedUserList = new ArrayList<>();
|
||||||
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
||||||
|
|
||||||
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
||||||
|
|
||||||
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
|
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +381,6 @@ 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) {
|
||||||
return ((UserDetails) principal).getUsername();
|
return ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
@@ -397,7 +403,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
String username = "CUSTOM_API_USER";
|
String username = "CUSTOM_API_USER";
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (!existingUser.isPresent()) {
|
if (!existingUser.isPresent()) {
|
||||||
// Create new user with API role
|
// Create new user with API role
|
||||||
User user = new User();
|
User user = new User();
|
||||||
|
|||||||
@@ -6,12 +6,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.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.sql.Connection;
|
import java.sql.*;
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.Statement;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package stirling.software.SPDF.config.security.database;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class ScheduledTasks {
|
public class ScheduledTasks {
|
||||||
|
|
||||||
@Autowired private DatabaseBackupHelper databaseBackupService;
|
private final DatabaseBackupHelper databaseBackupService;
|
||||||
|
|
||||||
|
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
|
||||||
|
this.databaseBackupService = databaseBackupService;
|
||||||
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 0 * * ?")
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
public void performBackup() throws IOException {
|
public void performBackup() throws IOException {
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
|||||||
@@ -5,19 +5,22 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class SessionScheduled {
|
public class SessionScheduled {
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
|
||||||
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
public SessionScheduled(SessionPersistentRegistry sessionPersistentRegistry) {
|
||||||
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0/5 * * * ?")
|
@Scheduled(cron = "0 0/5 * * * ?")
|
||||||
public void expireSessions() {
|
public void expireSessions() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
|
|
||||||
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
||||||
List<SessionInformation> sessionInformations =
|
List<SessionInformation> sessionInformations =
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import java.io.IOException;
|
|||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -18,35 +17,35 @@ import stirling.software.SPDF.service.LanguageService;
|
|||||||
@RequestMapping("/js")
|
@RequestMapping("/js")
|
||||||
public class AdditionalLanguageJsController {
|
public class AdditionalLanguageJsController {
|
||||||
|
|
||||||
@Autowired private LanguageService languageService;
|
private final LanguageService languageService;
|
||||||
|
|
||||||
|
public AdditionalLanguageJsController(LanguageService languageService) {
|
||||||
|
this.languageService = languageService;
|
||||||
|
}
|
||||||
|
|
||||||
@Hidden
|
@Hidden
|
||||||
@GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript")
|
@GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript")
|
||||||
public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException {
|
public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException {
|
||||||
List<String> supportedLanguages = languageService.getSupportedLanguages();
|
List<String> supportedLanguages = languageService.getSupportedLanguages();
|
||||||
|
|
||||||
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("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";");
|
writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";");
|
||||||
|
|
||||||
// Generiere die `getDetailedLanguageCode`-Funktion
|
// Generiere die `getDetailedLanguageCode`-Funktion
|
||||||
writer.println(
|
writer.println(
|
||||||
"""
|
"""
|
||||||
function getDetailedLanguageCode() {
|
function getDetailedLanguageCode() {
|
||||||
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
|
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
|
||||||
for (let lang of userLanguages) {
|
for (let lang of userLanguages) {
|
||||||
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
|
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
|
||||||
if (matchedLang) {
|
if (matchedLang) {
|
||||||
return matchedLang;
|
return matchedLang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback
|
||||||
|
return "en_GB";
|
||||||
}
|
}
|
||||||
}
|
""");
|
||||||
// Fallback
|
|
||||||
return "en_GB";
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,23 +8,19 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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;
|
||||||
@@ -34,19 +30,25 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
|||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/api/v1/database")
|
@RequestMapping("/api/v1/database")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@Tag(name = "Database", description = "Database APIs")
|
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||||
public class DatabaseController {
|
public class DatabaseController {
|
||||||
|
|
||||||
@Autowired DatabaseBackupHelper databaseBackupHelper;
|
private final DatabaseBackupHelper databaseBackupHelper;
|
||||||
|
|
||||||
|
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
|
||||||
|
this.databaseBackupHelper = databaseBackupHelper;
|
||||||
|
}
|
||||||
|
|
||||||
@Hidden
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "import-database")
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Import database backup",
|
summary = "Import a database backup file",
|
||||||
description = "This endpoint imports a database backup from a SQL file.")
|
description = "Uploads and imports a database backup SQL file.")
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "import-database")
|
||||||
public String importDatabase(
|
public String importDatabase(
|
||||||
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes)
|
@Parameter(description = "SQL file to import", required = true)
|
||||||
throws IllegalArgumentException, IOException {
|
@RequestParam("fileInput")
|
||||||
|
MultipartFile file,
|
||||||
|
RedirectAttributes redirectAttributes)
|
||||||
|
throws IOException {
|
||||||
if (file == null || file.isEmpty()) {
|
if (file == null || file.isEmpty()) {
|
||||||
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
|
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
|
||||||
return "redirect:/database";
|
return "redirect:/database";
|
||||||
@@ -69,13 +71,17 @@ public class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Hidden
|
@Hidden
|
||||||
|
@Operation(
|
||||||
|
summary = "Import database backup by filename",
|
||||||
|
description = "Imports a database backup file from the server using its file name.")
|
||||||
@GetMapping("/import-database-file/{fileName}")
|
@GetMapping("/import-database-file/{fileName}")
|
||||||
public String importDatabaseFromBackupUI(@PathVariable String fileName)
|
public String importDatabaseFromBackupUI(
|
||||||
throws IllegalArgumentException, IOException {
|
@Parameter(description = "Name of the file to import", required = true) @PathVariable
|
||||||
|
String fileName)
|
||||||
|
throws IOException {
|
||||||
if (fileName == null || fileName.isEmpty()) {
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
return "redirect:/database?error=fileNullOrEmpty";
|
return "redirect:/database?error=fileNullOrEmpty";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the file exists in the backup list
|
// Check if the file exists in the backup list
|
||||||
boolean fileExists =
|
boolean fileExists =
|
||||||
databaseBackupHelper.getBackupList().stream()
|
databaseBackupHelper.getBackupList().stream()
|
||||||
@@ -93,12 +99,13 @@ public class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Hidden
|
@Hidden
|
||||||
@GetMapping("/delete/{fileName}")
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Delete a database backup file",
|
summary = "Delete a database backup file",
|
||||||
description =
|
description = "Deletes a specified database backup file from the server.")
|
||||||
"This endpoint deletes a database backup file with the specified file name.")
|
@GetMapping("/delete/{fileName}")
|
||||||
public String deleteFile(@PathVariable String fileName) {
|
public String deleteFile(
|
||||||
|
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
|
||||||
|
String fileName) {
|
||||||
if (fileName == null || fileName.isEmpty()) {
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File must not be null or empty");
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
}
|
}
|
||||||
@@ -117,12 +124,13 @@ public class DatabaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Hidden
|
@Hidden
|
||||||
@GetMapping("/download/{fileName}")
|
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Download a database backup file",
|
summary = "Download a database backup file",
|
||||||
description =
|
description = "Downloads the specified database backup file from the server.")
|
||||||
"This endpoint downloads a database backup file with the specified file name.")
|
@GetMapping("/download/{fileName}")
|
||||||
public ResponseEntity<?> downloadFile(@PathVariable String fileName) {
|
public ResponseEntity<?> downloadFile(
|
||||||
|
@Parameter(description = "Name of the file to download", required = true) @PathVariable
|
||||||
|
String fileName) {
|
||||||
if (fileName == null || fileName.isEmpty()) {
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
throw new IllegalArgumentException("File must not be null or empty");
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
}
|
}
|
||||||
@@ -141,4 +149,22 @@ public class DatabaseController {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Create a database backup",
|
||||||
|
description =
|
||||||
|
"This endpoint triggers the creation of a database backup and redirects to the"
|
||||||
|
+ " database management page.")
|
||||||
|
@GetMapping("/createDatabaseBackup")
|
||||||
|
public String createDatabaseBackup() {
|
||||||
|
try {
|
||||||
|
log.info("Starting database backup creation...");
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
|
log.info("Database backup successfully created.");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error creating database backup: {}", e.getMessage(), e);
|
||||||
|
return "redirect:/database?error=" + e.getMessage();
|
||||||
|
}
|
||||||
|
return "redirect:/database?infoMessage=backupCreated";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import java.io.IOException;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import 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;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -20,7 +21,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
|
|||||||
@Hidden
|
@Hidden
|
||||||
public class SettingsController {
|
public class SettingsController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public SettingsController(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/update-enable-analytics")
|
@PostMapping("/update-enable-analytics")
|
||||||
@Hidden
|
@Hidden
|
||||||
@@ -32,7 +37,6 @@ public class SettingsController {
|
|||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||||
|
|
||||||
return ResponseEntity.ok("Updated");
|
return ResponseEntity.ok("Updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,74 @@ public class SplitPdfByChaptersController {
|
|||||||
this.pdfMetadataService = pdfMetadataService;
|
this.pdfMetadataService = pdfMetadataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<Bookmark> extractOutlineItems(
|
||||||
|
PDDocument sourceDocument,
|
||||||
|
PDOutlineItem current,
|
||||||
|
List<Bookmark> bookmarks,
|
||||||
|
PDOutlineItem nextParent,
|
||||||
|
int level,
|
||||||
|
int maxLevel)
|
||||||
|
throws Exception {
|
||||||
|
|
||||||
|
while (current != null) {
|
||||||
|
|
||||||
|
String currentTitle = current.getTitle().replace("/", "");
|
||||||
|
int firstPage =
|
||||||
|
sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument));
|
||||||
|
PDOutlineItem child = current.getFirstChild();
|
||||||
|
PDOutlineItem nextSibling = current.getNextSibling();
|
||||||
|
int endPage;
|
||||||
|
if (child != null && level < maxLevel) {
|
||||||
|
endPage =
|
||||||
|
sourceDocument
|
||||||
|
.getPages()
|
||||||
|
.indexOf(child.findDestinationPage(sourceDocument));
|
||||||
|
} else if (nextSibling != null) {
|
||||||
|
endPage =
|
||||||
|
sourceDocument
|
||||||
|
.getPages()
|
||||||
|
.indexOf(nextSibling.findDestinationPage(sourceDocument));
|
||||||
|
} else if (nextParent != null) {
|
||||||
|
|
||||||
|
endPage =
|
||||||
|
sourceDocument
|
||||||
|
.getPages()
|
||||||
|
.indexOf(nextParent.findDestinationPage(sourceDocument));
|
||||||
|
} else {
|
||||||
|
endPage = -2;
|
||||||
|
/*
|
||||||
|
happens when we have something like this:
|
||||||
|
Outline Item 2
|
||||||
|
Outline Item 2.1
|
||||||
|
Outline Item 2.1.1
|
||||||
|
Outline Item 2.2
|
||||||
|
Outline 2.2.1
|
||||||
|
Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling
|
||||||
|
Outline Item 3
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
if (!bookmarks.isEmpty()
|
||||||
|
&& bookmarks.get(bookmarks.size() - 1).getEndPage() == -2
|
||||||
|
&& firstPage
|
||||||
|
>= bookmarks
|
||||||
|
.get(bookmarks.size() - 1)
|
||||||
|
.getStartPage()) { // for handling the above-mentioned case
|
||||||
|
Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||||
|
previousBookmark.setEndPage(firstPage);
|
||||||
|
}
|
||||||
|
bookmarks.add(new Bookmark(currentTitle, firstPage, endPage));
|
||||||
|
|
||||||
|
// Recursively process children
|
||||||
|
if (child != null && level < maxLevel) {
|
||||||
|
extractOutlineItems(
|
||||||
|
sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
current = nextSibling;
|
||||||
|
}
|
||||||
|
return bookmarks;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDFs by Chapters",
|
summary = "Split PDFs by Chapters",
|
||||||
@@ -163,74 +231,6 @@ public class SplitPdfByChaptersController {
|
|||||||
return bookmarks;
|
return bookmarks;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Bookmark> extractOutlineItems(
|
|
||||||
PDDocument sourceDocument,
|
|
||||||
PDOutlineItem current,
|
|
||||||
List<Bookmark> bookmarks,
|
|
||||||
PDOutlineItem nextParent,
|
|
||||||
int level,
|
|
||||||
int maxLevel)
|
|
||||||
throws Exception {
|
|
||||||
|
|
||||||
while (current != null) {
|
|
||||||
|
|
||||||
String currentTitle = current.getTitle().replace("/", "");
|
|
||||||
int firstPage =
|
|
||||||
sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument));
|
|
||||||
PDOutlineItem child = current.getFirstChild();
|
|
||||||
PDOutlineItem nextSibling = current.getNextSibling();
|
|
||||||
int endPage;
|
|
||||||
if (child != null && level < maxLevel) {
|
|
||||||
endPage =
|
|
||||||
sourceDocument
|
|
||||||
.getPages()
|
|
||||||
.indexOf(child.findDestinationPage(sourceDocument));
|
|
||||||
} else if (nextSibling != null) {
|
|
||||||
endPage =
|
|
||||||
sourceDocument
|
|
||||||
.getPages()
|
|
||||||
.indexOf(nextSibling.findDestinationPage(sourceDocument));
|
|
||||||
} else if (nextParent != null) {
|
|
||||||
|
|
||||||
endPage =
|
|
||||||
sourceDocument
|
|
||||||
.getPages()
|
|
||||||
.indexOf(nextParent.findDestinationPage(sourceDocument));
|
|
||||||
} else {
|
|
||||||
endPage = -2;
|
|
||||||
/*
|
|
||||||
happens when we have something like this:
|
|
||||||
Outline Item 2
|
|
||||||
Outline Item 2.1
|
|
||||||
Outline Item 2.1.1
|
|
||||||
Outline Item 2.2
|
|
||||||
Outline 2.2.1
|
|
||||||
Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling
|
|
||||||
Outline Item 3
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
if (!bookmarks.isEmpty()
|
|
||||||
&& bookmarks.get(bookmarks.size() - 1).getEndPage() == -2
|
|
||||||
&& firstPage
|
|
||||||
>= bookmarks
|
|
||||||
.get(bookmarks.size() - 1)
|
|
||||||
.getStartPage()) { // for handling the above-mentioned case
|
|
||||||
Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1);
|
|
||||||
previousBookmark.setEndPage(firstPage);
|
|
||||||
}
|
|
||||||
bookmarks.add(new Bookmark(currentTitle, firstPage, endPage));
|
|
||||||
|
|
||||||
// Recursively process children
|
|
||||||
if (child != null && level < maxLevel) {
|
|
||||||
extractOutlineItems(
|
|
||||||
sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel);
|
|
||||||
}
|
|
||||||
|
|
||||||
current = nextSibling;
|
|
||||||
}
|
|
||||||
return bookmarks;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path createZipFile(
|
private Path createZipFile(
|
||||||
List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas)
|
List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -18,11 +17,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
|
||||||
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
@@ -45,9 +40,14 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
|
||||||
|
private final UserService userService;
|
||||||
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
@Autowired SessionPersistentRegistry sessionRegistry;
|
public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@@ -75,36 +75,27 @@ public class UserController {
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes)
|
RedirectAttributes redirectAttributes)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
if (!userService.isUsernameValid(newUsername)) {
|
if (!userService.isUsernameValid(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The username MUST be unique when renaming
|
// The username MUST be unique when renaming
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || 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();
|
||||||
|
|
||||||
if (user.getUsername().equals(newUsername)) {
|
if (user.getUsername().equals(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=usernameExists", true);
|
return new RedirectView("/account?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=usernameExists", true);
|
return new RedirectView("/account?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newUsername != null && newUsername.length() > 0) {
|
if (newUsername != null && newUsername.length() > 0) {
|
||||||
try {
|
try {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
@@ -112,10 +103,8 @@ public class UserController {
|
|||||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,24 +121,18 @@ public class UserController {
|
|||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
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 == null || 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();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
userService.changeFirstUse(user, false);
|
userService.changeFirstUse(user, false);
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,24 +149,17 @@ public class UserController {
|
|||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
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 == null || 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();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword", true);
|
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
|
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,17 +169,14 @@ public class UserController {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
Map<String, String> updates = new HashMap<>();
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
|
||||||
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
|
||||||
return "redirect:/account"; // Redirect to a page of your choice after updating
|
return "redirect:/account";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@@ -216,13 +189,10 @@ public class UserController {
|
|||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange)
|
boolean forceChange)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
|
|
||||||
if (!userService.isUsernameValid(username)) {
|
if (!userService.isUsernameValid(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 != null && user.getUsername().equalsIgnoreCase(username)) {
|
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||||
@@ -243,7 +213,6 @@ public class UserController {
|
|||||||
// If the role ID is not valid, redirect with an error message
|
// If the role ID is not valid, redirect with an error message
|
||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||||
userService.saveUser(username, AuthenticationType.SSO, role);
|
userService.saveUser(username, AuthenticationType.SSO, role);
|
||||||
} else {
|
} else {
|
||||||
@@ -252,9 +221,9 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
userService.saveUser(username, password, role, forceChange);
|
userService.saveUser(username, password, role, forceChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", // Redirect to account page after adding the user
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@@ -264,9 +233,7 @@ public class UserController {
|
|||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (!userOpt.isPresent()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
@@ -275,7 +242,6 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
|
||||||
@@ -292,11 +258,10 @@ public class UserController {
|
|||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", // Redirect to account page after adding the user
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@@ -306,9 +271,7 @@ public class UserController {
|
|||||||
@RequestParam("enabled") boolean enabled,
|
@RequestParam("enabled") boolean enabled,
|
||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (!userOpt.isPresent()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
@@ -317,15 +280,12 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeUserEnabled(user, enabled);
|
userService.changeUserEnabled(user, enabled);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
// Invalidate all sessions if the user is being disabled
|
// Invalidate all sessions if the user is being disabled
|
||||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
@@ -349,28 +309,24 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", // Redirect to account page after adding the user
|
||||||
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public RedirectView deleteUser(
|
public RedirectView deleteUser(
|
||||||
@PathVariable("username") String username, Authentication authentication) {
|
@PathVariable("username") String username, Authentication authentication) {
|
||||||
|
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate all sessions before deleting the user
|
// Invalidate all sessions before deleting the user
|
||||||
List<SessionInformation> sessionsInformations =
|
List<SessionInformation> sessionsInformations =
|
||||||
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
|
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
|
||||||
@@ -410,6 +366,4 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
return ResponseEntity.ok(apiKey);
|
return ResponseEntity.ok(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,13 @@ public class ConvertImgPDFController {
|
|||||||
String fitOption = request.getFitOption();
|
String fitOption = request.getFitOption();
|
||||||
String colorType = request.getColorType();
|
String colorType = request.getColorType();
|
||||||
boolean autoRotate = request.isAutoRotate();
|
boolean autoRotate = request.isAutoRotate();
|
||||||
|
// Handle Null entries for formdata
|
||||||
|
if (colorType == null || colorType.isBlank()) {
|
||||||
|
colorType = "color";
|
||||||
|
}
|
||||||
|
if (fitOption == null || fitOption.isEmpty()) {
|
||||||
|
fitOption = "fillPage";
|
||||||
|
}
|
||||||
// Convert the file to PDF and get the resulting bytes
|
// Convert the file to PDF and get the resulting bytes
|
||||||
byte[] bytes =
|
byte[] bytes =
|
||||||
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);
|
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertOfficeController {
|
public class ConvertOfficeController {
|
||||||
|
|
||||||
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||||
// Check for valid file extension
|
// Check for valid file extension
|
||||||
String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||||
@@ -78,13 +85,6 @@ public class ConvertOfficeController {
|
|||||||
return fileExtension.matches(extensionPattern);
|
return fileExtension.matches(extensionPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a file to a PDF using LibreOffice",
|
summary = "Convert a file to a PDF using LibreOffice",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
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;
|
||||||
@@ -26,9 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
// @RequestMapping("/api/v1/convert")
|
// @RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToBookController {
|
public class ConvertPDFToBookController {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean bookAndHtmlFormatsInstalled;
|
private final boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
|
public ConvertPDFToBookController(
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
|
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -39,16 +42,13 @@ public class ConvertPDFToBookController {
|
|||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
|
||||||
if (!bookAndHtmlFormatsInstalled) {
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileInput == null) {
|
if (fileInput == null) {
|
||||||
throw new IllegalArgumentException("Please provide a file for conversion.");
|
throw new IllegalArgumentException("Please provide a file for conversion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the output format
|
// Validate the output format
|
||||||
String outputFormat = request.getOutputFormat().toLowerCase();
|
String outputFormat = request.getOutputFormat().toLowerCase();
|
||||||
List<String> allowedFormats =
|
List<String> allowedFormats =
|
||||||
@@ -58,28 +58,24 @@ public class ConvertPDFToBookController {
|
|||||||
if (!allowedFormats.contains(outputFormat)) {
|
if (!allowedFormats.contains(outputFormat)) {
|
||||||
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
|
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] outputFileBytes;
|
byte[] outputFileBytes;
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
Path tempOutputFile =
|
Path tempOutputFile =
|
||||||
Files.createTempFile(
|
Files.createTempFile(
|
||||||
"output_",
|
"output_", // Use the output format for the file extension
|
||||||
"." + outputFormat); // Use the output format for the file extension
|
"." + outputFormat);
|
||||||
Path tempInputFile = null;
|
Path tempInputFile = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create temp input file from the provided PDF
|
// Create temp input file from the provided PDF
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf"); // Assuming input is always PDF
|
// Assuming input is always PDF
|
||||||
|
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
Files.write(tempInputFile, fileInput.getBytes());
|
Files.write(tempInputFile, fileInput.getBytes());
|
||||||
|
|
||||||
command.add("ebook-convert");
|
command.add("ebook-convert");
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
outputFileBytes = Files.readAllBytes(tempOutputFile);
|
outputFileBytes = Files.readAllBytes(tempOutputFile);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temporary files
|
// Clean up temporary files
|
||||||
@@ -88,13 +84,12 @@ public class ConvertPDFToBookController {
|
|||||||
}
|
}
|
||||||
Files.deleteIfExists(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "."
|
+ "."
|
||||||
+ outputFormat; // Remove file extension and append .pdf
|
+ // Remove file extension and append .pdf
|
||||||
|
outputFormat;
|
||||||
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,16 +46,6 @@ public class AutoRenameController {
|
|||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
PDFTextStripper reader =
|
PDFTextStripper reader =
|
||||||
new PDFTextStripper() {
|
new PDFTextStripper() {
|
||||||
class LineInfo {
|
|
||||||
String text;
|
|
||||||
float fontSize;
|
|
||||||
|
|
||||||
LineInfo(String text, float fontSize) {
|
|
||||||
this.text = text;
|
|
||||||
this.fontSize = fontSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<LineInfo> lineInfos = new ArrayList<>();
|
List<LineInfo> lineInfos = new ArrayList<>();
|
||||||
StringBuilder lineBuilder = new StringBuilder();
|
StringBuilder lineBuilder = new StringBuilder();
|
||||||
float lastY = -1;
|
float lastY = -1;
|
||||||
@@ -122,6 +112,16 @@ public class AutoRenameController {
|
|||||||
.text)
|
.text)
|
||||||
: null);
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LineInfo {
|
||||||
|
String text;
|
||||||
|
float fontSize;
|
||||||
|
|
||||||
|
LineInfo(String text, float fontSize) {
|
||||||
|
this.text = text;
|
||||||
|
this.fontSize = fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
String header = reader.getText(document);
|
String header = reader.getText(document);
|
||||||
|
|||||||
@@ -23,12 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.google.zxing.BinaryBitmap;
|
import com.google.zxing.*;
|
||||||
import com.google.zxing.LuminanceSource;
|
|
||||||
import com.google.zxing.MultiFormatReader;
|
|
||||||
import com.google.zxing.NotFoundException;
|
|
||||||
import com.google.zxing.PlanarYUVLuminanceSource;
|
|
||||||
import com.google.zxing.Result;
|
|
||||||
import com.google.zxing.common.HybridBinarizer;
|
import com.google.zxing.common.HybridBinarizer;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
@@ -56,6 +51,52 @@ public class AutoSplitPdfController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String decodeQRCode(BufferedImage bufferedImage) {
|
||||||
|
LuminanceSource source;
|
||||||
|
|
||||||
|
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
|
||||||
|
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||||
|
source =
|
||||||
|
new PlanarYUVLuminanceSource(
|
||||||
|
pixels,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
false);
|
||||||
|
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
||||||
|
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
||||||
|
byte[] newPixels = new byte[pixels.length];
|
||||||
|
for (int i = 0; i < pixels.length; i++) {
|
||||||
|
newPixels[i] = (byte) (pixels[i] & 0xff);
|
||||||
|
}
|
||||||
|
source =
|
||||||
|
new PlanarYUVLuminanceSource(
|
||||||
|
newPixels,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
bufferedImage.getWidth(),
|
||||||
|
bufferedImage.getHeight(),
|
||||||
|
false);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
|
|
||||||
|
try {
|
||||||
|
Result result = new MultiFormatReader().decode(bitmap);
|
||||||
|
return result.getText();
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
return null; // there is no QR code in the image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents",
|
summary = "Auto split PDF pages into separate documents",
|
||||||
@@ -154,50 +195,4 @@ public class AutoSplitPdfController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String decodeQRCode(BufferedImage bufferedImage) {
|
|
||||||
LuminanceSource source;
|
|
||||||
|
|
||||||
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
|
|
||||||
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
|
|
||||||
source =
|
|
||||||
new PlanarYUVLuminanceSource(
|
|
||||||
pixels,
|
|
||||||
bufferedImage.getWidth(),
|
|
||||||
bufferedImage.getHeight(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
bufferedImage.getWidth(),
|
|
||||||
bufferedImage.getHeight(),
|
|
||||||
false);
|
|
||||||
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
|
||||||
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
|
||||||
byte[] newPixels = new byte[pixels.length];
|
|
||||||
for (int i = 0; i < pixels.length; i++) {
|
|
||||||
newPixels[i] = (byte) (pixels[i] & 0xff);
|
|
||||||
}
|
|
||||||
source =
|
|
||||||
new PlanarYUVLuminanceSource(
|
|
||||||
newPixels,
|
|
||||||
bufferedImage.getWidth(),
|
|
||||||
bufferedImage.getHeight(),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
bufferedImage.getWidth(),
|
|
||||||
bufferedImage.getHeight(),
|
|
||||||
false);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
|
||||||
|
|
||||||
try {
|
|
||||||
Result result = new MultiFormatReader().decode(bitmap);
|
|
||||||
return result.getText();
|
|
||||||
} catch (NotFoundException e) {
|
|
||||||
return null; // there is no QR code in the image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,32 @@ public class BlankPageController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isBlankImage(
|
||||||
|
BufferedImage image, int threshold, double whitePercent, int blurSize) {
|
||||||
|
if (image == null) {
|
||||||
|
log.info("Error: Image is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to binary image based on the threshold
|
||||||
|
int whitePixels = 0;
|
||||||
|
int totalPixels = image.getWidth() * image.getHeight();
|
||||||
|
|
||||||
|
for (int i = 0; i < image.getHeight(); i++) {
|
||||||
|
for (int j = 0; j < image.getWidth(); j++) {
|
||||||
|
int color = image.getRGB(j, i) & 0xFF;
|
||||||
|
if (color >= 255 - threshold) {
|
||||||
|
whitePixels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
|
||||||
|
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
|
||||||
|
|
||||||
|
return whitePixelPercentage >= whitePercent;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Remove blank pages from a PDF file",
|
summary = "Remove blank pages from a PDF file",
|
||||||
@@ -143,30 +169,4 @@ public class BlankPageController {
|
|||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isBlankImage(
|
|
||||||
BufferedImage image, int threshold, double whitePercent, int blurSize) {
|
|
||||||
if (image == null) {
|
|
||||||
log.info("Error: Image is null");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to binary image based on the threshold
|
|
||||||
int whitePixels = 0;
|
|
||||||
int totalPixels = image.getWidth() * image.getHeight();
|
|
||||||
|
|
||||||
for (int i = 0; i < image.getHeight(); i++) {
|
|
||||||
for (int j = 0; j < image.getWidth(); j++) {
|
|
||||||
int color = image.getRGB(j, i) & 0xFF;
|
|
||||||
if (color >= 255 - threshold) {
|
|
||||||
whitePixels++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
|
|
||||||
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
|
|
||||||
|
|
||||||
return whitePixelPercentage >= whitePercent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Image;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class ExtractImageScansController {
|
public class ExtractImageScansController {
|
||||||
|
|
||||||
|
private static final String REPLACEFIRST = "[.][^.]+$";
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract image scans from an input file",
|
summary = "Extract image scans from an input file",
|
||||||
@@ -221,6 +223,4 @@ public class ExtractImageScansController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String REPLACEFIRST = "[.][^.]+$";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.AlphaComposite;
|
import java.awt.*;
|
||||||
import java.awt.BasicStroke;
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.GradientPaint;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.geom.AffineTransform;
|
import java.awt.geom.AffineTransform;
|
||||||
import java.awt.geom.Ellipse2D;
|
import java.awt.geom.Ellipse2D;
|
||||||
import java.awt.geom.Path2D;
|
import java.awt.geom.Path2D;
|
||||||
import java.awt.image.AffineTransformOp;
|
import java.awt.image.*;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.awt.image.BufferedImageOp;
|
|
||||||
import java.awt.image.ConvolveOp;
|
|
||||||
import java.awt.image.Kernel;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
|||||||
@@ -13,11 +13,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.InitBinder;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.BufferedReader;
|
import java.io.*;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -24,7 +16,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -48,12 +39,14 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class OCRController {
|
public class OCRController {
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@Autowired
|
public OCRController(
|
||||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
ApplicationProperties applicationProperties,
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,13 +71,11 @@ public class OCRController {
|
|||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
List<String> languages = request.getLanguages();
|
List<String> languages = request.getLanguages();
|
||||||
String ocrType = request.getOcrType();
|
String ocrType = request.getOcrType();
|
||||||
|
|
||||||
Path tempDir = Files.createTempDirectory("ocr_process");
|
Path tempDir = Files.createTempDirectory("ocr_process");
|
||||||
Path tempInputFile = tempDir.resolve("input.pdf");
|
Path tempInputFile = tempDir.resolve("input.pdf");
|
||||||
Path tempOutputDir = tempDir.resolve("output");
|
Path tempOutputDir = tempDir.resolve("output");
|
||||||
Path tempImagesDir = tempDir.resolve("images");
|
Path tempImagesDir = tempDir.resolve("images");
|
||||||
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
||||||
|
|
||||||
Files.createDirectories(tempOutputDir);
|
Files.createDirectories(tempOutputDir);
|
||||||
Files.createDirectories(tempImagesDir);
|
Files.createDirectories(tempImagesDir);
|
||||||
Process process = null;
|
Process process = null;
|
||||||
@@ -93,39 +84,32 @@ public class OCRController {
|
|||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
PDFMergerUtility merger = new PDFMergerUtility();
|
PDFMergerUtility merger = new PDFMergerUtility();
|
||||||
merger.setDestinationFileName(finalOutputFile.toString());
|
merger.setDestinationFileName(finalOutputFile.toString());
|
||||||
|
|
||||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
int pageCount = document.getNumberOfPages();
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
||||||
PDPage page = document.getPage(pageNum);
|
PDPage page = document.getPage(pageNum);
|
||||||
boolean hasText = false;
|
boolean hasText = false;
|
||||||
|
|
||||||
// Check for existing text
|
// Check for existing text
|
||||||
try (PDDocument tempDoc = new PDDocument()) {
|
try (PDDocument tempDoc = new PDDocument()) {
|
||||||
tempDoc.addPage(page);
|
tempDoc.addPage(page);
|
||||||
PDFTextStripper stripper = new PDFTextStripper();
|
PDFTextStripper stripper = new PDFTextStripper();
|
||||||
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean shouldOcr =
|
boolean shouldOcr =
|
||||||
switch (ocrType) {
|
switch (ocrType) {
|
||||||
case "skip-text" -> !hasText;
|
case "skip-text" -> !hasText;
|
||||||
case "force-ocr" -> true;
|
case "force-ocr" -> true;
|
||||||
default -> true;
|
default -> true;
|
||||||
};
|
};
|
||||||
|
|
||||||
Path pageOutputPath =
|
Path pageOutputPath =
|
||||||
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
|
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
|
||||||
|
|
||||||
if (shouldOcr) {
|
if (shouldOcr) {
|
||||||
// Convert page to image
|
// Convert page to image
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
||||||
Path imagePath =
|
Path imagePath =
|
||||||
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
|
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
|
||||||
ImageIO.write(image, "png", imagePath.toFile());
|
ImageIO.write(image, "png", imagePath.toFile());
|
||||||
|
|
||||||
// Build OCR command
|
// Build OCR command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("tesseract");
|
command.add("tesseract");
|
||||||
@@ -136,11 +120,10 @@ public class OCRController {
|
|||||||
.toString());
|
.toString());
|
||||||
command.add("-l");
|
command.add("-l");
|
||||||
command.add(String.join("+", languages));
|
command.add(String.join("+", languages));
|
||||||
command.add("pdf"); // Always output PDF
|
// Always output PDF
|
||||||
|
command.add("pdf");
|
||||||
ProcessBuilder pb = new ProcessBuilder(command);
|
ProcessBuilder pb = new ProcessBuilder(command);
|
||||||
process = pb.start();
|
process = pb.start();
|
||||||
|
|
||||||
// Capture any error output
|
// Capture any error output
|
||||||
try (BufferedReader reader =
|
try (BufferedReader reader =
|
||||||
new BufferedReader(
|
new BufferedReader(
|
||||||
@@ -150,13 +133,11 @@ public class OCRController {
|
|||||||
log.debug("Tesseract: {}", line);
|
log.debug("Tesseract: {}", line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int exitCode = process.waitFor();
|
int exitCode = process.waitFor();
|
||||||
if (exitCode != 0) {
|
if (exitCode != 0) {
|
||||||
throw new RuntimeException(
|
throw new RuntimeException(
|
||||||
"Tesseract failed with exit code: " + exitCode);
|
"Tesseract failed with exit code: " + exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add OCR'd PDF to merger
|
// Add OCR'd PDF to merger
|
||||||
merger.addSource(pageOutputPath.toFile());
|
merger.addSource(pageOutputPath.toFile());
|
||||||
} else {
|
} else {
|
||||||
@@ -169,29 +150,24 @@ public class OCRController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge all pages into final PDF
|
// Merge all pages into final PDF
|
||||||
merger.mergeDocuments(null);
|
merger.mergeDocuments(null);
|
||||||
|
|
||||||
// Read the final PDF file
|
// Read the final PDF file
|
||||||
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_OCR.pdf";
|
+ "_OCR.pdf";
|
||||||
|
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(
|
.header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
"attachment; filename=\"" + outputFilename + "\"")
|
"attachment; filename=\"" + outputFilename + "\"")
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
.body(pdfContent);
|
.body(pdfContent);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
if (process != null) {
|
if (process != null) {
|
||||||
process.destroy();
|
process.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temporary files
|
// Clean up temporary files
|
||||||
deleteDirectory(tempDir);
|
deleteDirectory(tempDir);
|
||||||
}
|
}
|
||||||
@@ -203,17 +179,14 @@ public class OCRController {
|
|||||||
log.warn("File {} does not exist, skipping", file);
|
log.warn("File {} does not exist, skipping", file);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (FileInputStream fis = new FileInputStream(file)) {
|
try (FileInputStream fis = new FileInputStream(file)) {
|
||||||
ZipEntry zipEntry = new ZipEntry(filename);
|
ZipEntry zipEntry = new ZipEntry(filename);
|
||||||
zipOut.putNextEntry(zipEntry);
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
int length;
|
int length;
|
||||||
while ((length = fis.read(buffer)) >= 0) {
|
while ((length = fis.read(buffer)) >= 0) {
|
||||||
zipOut.write(buffer, 0, length);
|
zipOut.write(buffer, 0, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Graphics;
|
import java.awt.*;
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.print.PageFormat;
|
import java.awt.print.PageFormat;
|
||||||
import java.awt.print.Printable;
|
import java.awt.print.Printable;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.*;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
|||||||
@@ -30,17 +30,24 @@ public class ApiDocService {
|
|||||||
|
|
||||||
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
|
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
|
||||||
|
|
||||||
@Autowired private ServletContext servletContext;
|
private final ServletContext servletContext;
|
||||||
|
private final UserServiceInterface userService;
|
||||||
|
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||||
|
JsonNode apiDocsJsonRootNode;
|
||||||
|
|
||||||
|
public ApiDocService(
|
||||||
|
ServletContext servletContext,
|
||||||
|
@Autowired(required = false) UserServiceInterface userService) {
|
||||||
|
this.servletContext = servletContext;
|
||||||
|
this.userService = userService;
|
||||||
|
}
|
||||||
|
|
||||||
private String getApiDocsUrl() {
|
private String getApiDocsUrl() {
|
||||||
String contextPath = servletContext.getContextPath();
|
String contextPath = servletContext.getContextPath();
|
||||||
String port = SPdfApplication.getStaticPort();
|
String port = SPdfApplication.getStaticPort();
|
||||||
|
|
||||||
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
|
||||||
|
|
||||||
public List<String> getExtensionTypes(boolean output, String operationName) {
|
public List<String> getExtensionTypes(boolean output, String operationName) {
|
||||||
if (outputToFileTypes.size() == 0) {
|
if (outputToFileTypes.size() == 0) {
|
||||||
outputToFileTypes.put("PDF", Arrays.asList("pdf"));
|
outputToFileTypes.put("PDF", Arrays.asList("pdf"));
|
||||||
@@ -64,14 +71,12 @@ public class ApiDocService {
|
|||||||
"BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx"));
|
"BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx"));
|
||||||
// type.
|
// type.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
|
if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
|
||||||
loadApiDocumentation();
|
loadApiDocumentation();
|
||||||
}
|
}
|
||||||
if (!apiDocumentation.containsKey(operationName)) {
|
if (!apiDocumentation.containsKey(operationName)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
||||||
String description = endpoint.getDescription();
|
String description = endpoint.getDescription();
|
||||||
Pattern pattern = null;
|
Pattern pattern = null;
|
||||||
@@ -90,16 +95,11 @@ public class ApiDocService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private UserServiceInterface userService;
|
|
||||||
|
|
||||||
private String getApiKeyForUser() {
|
private String getApiKeyForUser() {
|
||||||
if (userService == null) return "";
|
if (userService == null) return "";
|
||||||
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonNode apiDocsJsonRootNode;
|
|
||||||
|
|
||||||
// @EventListener(ApplicationReadyEvent.class)
|
// @EventListener(ApplicationReadyEvent.class)
|
||||||
private synchronized void loadApiDocumentation() {
|
private synchronized void loadApiDocumentation() {
|
||||||
String apiDocsJson = "";
|
String apiDocsJson = "";
|
||||||
@@ -110,15 +110,12 @@ public class ApiDocService {
|
|||||||
headers.set("X-API-KEY", apiKey);
|
headers.set("X-API-KEY", apiKey);
|
||||||
}
|
}
|
||||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||||
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response =
|
||||||
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
|
||||||
apiDocsJson = response.getBody();
|
apiDocsJson = response.getBody();
|
||||||
|
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
|
||||||
|
|
||||||
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
JsonNode paths = apiDocsJsonRootNode.path("paths");
|
||||||
paths.fields()
|
paths.fields()
|
||||||
.forEachRemaining(
|
.forEachRemaining(
|
||||||
@@ -155,19 +152,15 @@ public class ApiDocService {
|
|||||||
if (!apiDocumentation.containsKey(operationName)) {
|
if (!apiDocumentation.containsKey(operationName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
ApiEndpoint endpoint = apiDocumentation.get(operationName);
|
||||||
String description = endpoint.getDescription();
|
String description = endpoint.getDescription();
|
||||||
|
|
||||||
Pattern pattern = Pattern.compile("Type:(\\w+)");
|
Pattern pattern = Pattern.compile("Type:(\\w+)");
|
||||||
Matcher matcher = pattern.matcher(description);
|
Matcher matcher = pattern.matcher(description);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
String type = matcher.group(1);
|
String type = matcher.group(1);
|
||||||
return type.startsWith("MI");
|
return type.startsWith("MI");
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model class for API Endpoint
|
// Model class for API Endpoint
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import java.util.Map;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -37,17 +36,27 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class PipelineController {
|
public class PipelineController {
|
||||||
|
|
||||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||||
|
|
||||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||||
@Autowired PipelineProcessor processor;
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private final PipelineProcessor processor;
|
||||||
|
|
||||||
@Autowired private ObjectMapper objectMapper;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public PipelineController(
|
||||||
|
PipelineProcessor processor,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
ObjectMapper objectMapper) {
|
||||||
|
this.processor = processor;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/handleData")
|
@PostMapping("/handleData")
|
||||||
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
|
||||||
throws JsonMappingException, JsonProcessingException {
|
throws JsonMappingException, JsonProcessingException {
|
||||||
|
|
||||||
MultipartFile[] files = request.getFileInput();
|
MultipartFile[] files = request.getFileInput();
|
||||||
String jsonString = request.getJson();
|
String jsonString = request.getJson();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
@@ -68,26 +77,21 @@ public class PipelineController {
|
|||||||
byte[] bytes = new byte[(int) singleFile.contentLength()];
|
byte[] bytes = new byte[(int) singleFile.contentLength()];
|
||||||
is.read(bytes);
|
is.read(bytes);
|
||||||
is.close();
|
is.close();
|
||||||
|
|
||||||
log.info("Returning single file response...");
|
log.info("Returning single file response...");
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} else if (outputFiles == null) {
|
} else if (outputFiles == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a ByteArrayOutputStream to hold the zip
|
// Create a ByteArrayOutputStream to hold the zip
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
ZipOutputStream zipOut = new ZipOutputStream(baos);
|
ZipOutputStream zipOut = new ZipOutputStream(baos);
|
||||||
|
|
||||||
// A map to keep track of filenames and their counts
|
// A map to keep track of filenames and their counts
|
||||||
Map<String, Integer> filenameCount = new HashMap<>();
|
Map<String, Integer> filenameCount = new HashMap<>();
|
||||||
|
|
||||||
// Loop through each file and add it to the zip
|
// Loop through each file and add it to the zip
|
||||||
for (Resource file : outputFiles) {
|
for (Resource file : outputFiles) {
|
||||||
String originalFilename = file.getFilename();
|
String originalFilename = file.getFilename();
|
||||||
String filename = originalFilename;
|
String filename = originalFilename;
|
||||||
|
|
||||||
// Check if the filename already exists, and modify it if necessary
|
// Check if the filename already exists, and modify it if necessary
|
||||||
if (filenameCount.containsKey(originalFilename)) {
|
if (filenameCount.containsKey(originalFilename)) {
|
||||||
int count = filenameCount.get(originalFilename);
|
int count = filenameCount.get(originalFilename);
|
||||||
@@ -98,24 +102,18 @@ public class PipelineController {
|
|||||||
} else {
|
} else {
|
||||||
filenameCount.put(originalFilename, 1);
|
filenameCount.put(originalFilename, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
ZipEntry zipEntry = new ZipEntry(filename);
|
ZipEntry zipEntry = new ZipEntry(filename);
|
||||||
zipOut.putNextEntry(zipEntry);
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
|
||||||
// Read the file into a byte array
|
// Read the file into a byte array
|
||||||
InputStream is = file.getInputStream();
|
InputStream is = file.getInputStream();
|
||||||
byte[] bytes = new byte[(int) file.contentLength()];
|
byte[] bytes = new byte[(int) file.contentLength()];
|
||||||
is.read(bytes);
|
is.read(bytes);
|
||||||
|
|
||||||
// Write the bytes of the file to the zip
|
// Write the bytes of the file to the zip
|
||||||
zipOut.write(bytes, 0, bytes.length);
|
zipOut.write(bytes, 0, bytes.length);
|
||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
|
|
||||||
is.close();
|
is.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
zipOut.close();
|
zipOut.close();
|
||||||
|
|
||||||
log.info("Returning zipped file response...");
|
log.info("Returning zipped file response...");
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.boasToWebResponse(
|
||||||
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -34,19 +33,31 @@ import stirling.software.SPDF.utils.FileMonitor;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class PipelineDirectoryProcessor {
|
public class PipelineDirectoryProcessor {
|
||||||
|
|
||||||
@Autowired private ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
@Autowired private ApiDocService apiDocService;
|
|
||||||
@Autowired PipelineProcessor processor;
|
|
||||||
@Autowired FileMonitor fileMonitor;
|
|
||||||
|
|
||||||
final String watchedFoldersDir;
|
private final ApiDocService apiDocService;
|
||||||
final String finishedFoldersDir;
|
|
||||||
|
private final PipelineProcessor processor;
|
||||||
|
|
||||||
|
private final FileMonitor fileMonitor;
|
||||||
|
|
||||||
|
private final String watchedFoldersDir;
|
||||||
|
|
||||||
|
private final String finishedFoldersDir;
|
||||||
|
|
||||||
public PipelineDirectoryProcessor(
|
public PipelineDirectoryProcessor(
|
||||||
|
ObjectMapper objectMapper,
|
||||||
|
ApiDocService apiDocService,
|
||||||
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
|
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
|
||||||
@Qualifier("finishedFoldersDir") String finishedFoldersDir) {
|
@Qualifier("finishedFoldersDir") String finishedFoldersDir,
|
||||||
|
PipelineProcessor processor,
|
||||||
|
FileMonitor fileMonitor) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
this.apiDocService = apiDocService;
|
||||||
this.watchedFoldersDir = watchedFoldersDir;
|
this.watchedFoldersDir = watchedFoldersDir;
|
||||||
this.finishedFoldersDir = finishedFoldersDir;
|
this.finishedFoldersDir = finishedFoldersDir;
|
||||||
|
this.processor = processor;
|
||||||
|
this.fileMonitor = fileMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedRate = 60000)
|
@Scheduled(fixedRate = 60000)
|
||||||
@@ -81,13 +92,11 @@ public class PipelineDirectoryProcessor {
|
|||||||
public void handleDirectory(Path dir) throws IOException {
|
public void handleDirectory(Path dir) throws IOException {
|
||||||
log.info("Handling directory: {}", dir);
|
log.info("Handling directory: {}", dir);
|
||||||
Path processingDir = createProcessingDirectory(dir);
|
Path processingDir = createProcessingDirectory(dir);
|
||||||
|
|
||||||
Optional<Path> jsonFileOptional = findJsonFile(dir);
|
Optional<Path> jsonFileOptional = findJsonFile(dir);
|
||||||
if (!jsonFileOptional.isPresent()) {
|
if (!jsonFileOptional.isPresent()) {
|
||||||
log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
|
log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path jsonFile = jsonFileOptional.get();
|
Path jsonFile = jsonFileOptional.get();
|
||||||
PipelineConfig config = readAndParseJson(jsonFile);
|
PipelineConfig config = readAndParseJson(jsonFile);
|
||||||
processPipelineOperations(dir, processingDir, jsonFile, config);
|
processPipelineOperations(dir, processingDir, jsonFile, config);
|
||||||
@@ -166,13 +175,11 @@ public class PipelineDirectoryProcessor {
|
|||||||
private Path resolveUniqueFilePath(Path directory, String originalFileName) {
|
private Path resolveUniqueFilePath(Path directory, String originalFileName) {
|
||||||
Path filePath = directory.resolve(originalFileName);
|
Path filePath = directory.resolve(originalFileName);
|
||||||
int counter = 1;
|
int counter = 1;
|
||||||
|
|
||||||
while (Files.exists(filePath)) {
|
while (Files.exists(filePath)) {
|
||||||
String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")");
|
String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")");
|
||||||
filePath = directory.resolve(newName);
|
filePath = directory.resolve(newName);
|
||||||
counter++;
|
counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,17 +218,14 @@ public class PipelineDirectoryProcessor {
|
|||||||
for (Resource resource : resources) {
|
for (Resource resource : resources) {
|
||||||
String outputFileName = createOutputFileName(resource, config);
|
String outputFileName = createOutputFileName(resource, config);
|
||||||
Path outputPath = determineOutputPath(config, dir);
|
Path outputPath = determineOutputPath(config, dir);
|
||||||
|
|
||||||
if (!Files.exists(outputPath)) {
|
if (!Files.exists(outputPath)) {
|
||||||
Files.createDirectories(outputPath);
|
Files.createDirectories(outputPath);
|
||||||
log.info("Created directory: {}", outputPath);
|
log.info("Created directory: {}", outputPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
Path outputFile = outputPath.resolve(outputFileName);
|
Path outputFile = outputPath.resolve(outputFileName);
|
||||||
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
|
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
|
||||||
os.write(((ByteArrayResource) resource).getByteArray());
|
os.write(((ByteArrayResource) resource).getByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("File moved and renamed to {}", outputFile);
|
log.info("File moved and renamed to {}", outputFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,7 +234,6 @@ public class PipelineDirectoryProcessor {
|
|||||||
String resourceName = resource.getFilename();
|
String resourceName = resource.getFilename();
|
||||||
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
|
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
|
||||||
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
|
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
String outputFileName =
|
String outputFileName =
|
||||||
config.getOutputPattern()
|
config.getOutputPattern()
|
||||||
.replace("{filename}", baseName)
|
.replace("{filename}", baseName)
|
||||||
@@ -245,7 +248,6 @@ public class PipelineDirectoryProcessor {
|
|||||||
.format(DateTimeFormatter.ofPattern("HHmmss")))
|
.format(DateTimeFormatter.ofPattern("HHmmss")))
|
||||||
+ "."
|
+ "."
|
||||||
+ extension;
|
+ extension;
|
||||||
|
|
||||||
return outputFileName;
|
return outputFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +257,6 @@ public class PipelineDirectoryProcessor {
|
|||||||
.replace("{outputFolder}", finishedFoldersDir)
|
.replace("{outputFolder}", finishedFoldersDir)
|
||||||
.replace("{folderName}", dir.toString())
|
.replace("{folderName}", dir.toString())
|
||||||
.replaceAll("\\\\?watchedFolders", "");
|
.replaceAll("\\\\?watchedFolders", "");
|
||||||
|
|
||||||
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
|
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.pipeline;
|
package stirling.software.SPDF.controller.api.pipeline;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -22,12 +18,7 @@ import java.util.zip.ZipInputStream;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.*;
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
import org.springframework.util.LinkedMultiValueMap;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
@@ -48,12 +39,39 @@ import stirling.software.SPDF.model.Role;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class PipelineProcessor {
|
public class PipelineProcessor {
|
||||||
|
|
||||||
@Autowired private ApiDocService apiDocService;
|
private final ApiDocService apiDocService;
|
||||||
|
|
||||||
@Autowired(required = false)
|
private final UserServiceInterface userService;
|
||||||
private UserServiceInterface userService;
|
|
||||||
|
|
||||||
@Autowired private ServletContext servletContext;
|
private final ServletContext servletContext;
|
||||||
|
|
||||||
|
public PipelineProcessor(
|
||||||
|
ApiDocService apiDocService,
|
||||||
|
@Autowired(required = false) UserServiceInterface userService,
|
||||||
|
ServletContext servletContext) {
|
||||||
|
this.apiDocService = apiDocService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.servletContext = servletContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String removeTrailingNaming(String filename) {
|
||||||
|
// Splitting filename into name and extension
|
||||||
|
int dotIndex = filename.lastIndexOf(".");
|
||||||
|
if (dotIndex == -1) {
|
||||||
|
// No extension found
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
String name = filename.substring(0, dotIndex);
|
||||||
|
String extension = filename.substring(dotIndex);
|
||||||
|
// Finding the last underscore
|
||||||
|
int underscoreIndex = name.lastIndexOf("_");
|
||||||
|
if (underscoreIndex == -1) {
|
||||||
|
// No underscore found
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
// Removing the last part and reattaching the extension
|
||||||
|
return name.substring(0, underscoreIndex) + extension;
|
||||||
|
}
|
||||||
|
|
||||||
private String getApiKeyForUser() {
|
private String getApiKeyForUser() {
|
||||||
if (userService == null) return "";
|
if (userService == null) return "";
|
||||||
@@ -63,22 +81,17 @@ public class PipelineProcessor {
|
|||||||
private String getBaseUrl() {
|
private String getBaseUrl() {
|
||||||
String contextPath = servletContext.getContextPath();
|
String contextPath = servletContext.getContextPath();
|
||||||
String port = SPdfApplication.getStaticPort();
|
String port = SPdfApplication.getStaticPort();
|
||||||
|
|
||||||
return "http://localhost:" + port + contextPath + "/";
|
return "http://localhost:" + port + contextPath + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
|
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
||||||
PrintStream logPrintStream = new PrintStream(logStream);
|
PrintStream logPrintStream = new PrintStream(logStream);
|
||||||
|
|
||||||
boolean hasErrors = false;
|
boolean hasErrors = false;
|
||||||
|
|
||||||
for (PipelineOperation pipelineOperation : config.getOperations()) {
|
for (PipelineOperation pipelineOperation : config.getOperations()) {
|
||||||
String operation = pipelineOperation.getOperation();
|
String operation = pipelineOperation.getOperation();
|
||||||
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
"Running operation: {} isMultiInputOperation {}",
|
"Running operation: {} isMultiInputOperation {}",
|
||||||
operation,
|
operation,
|
||||||
@@ -89,9 +102,7 @@ public class PipelineProcessor {
|
|||||||
inputFileTypes = new ArrayList<String>(Arrays.asList("ALL"));
|
inputFileTypes = new ArrayList<String>(Arrays.asList("ALL"));
|
||||||
}
|
}
|
||||||
// List outputFileTypes = apiDocService.getExtensionTypes(true, operation);
|
// List outputFileTypes = apiDocService.getExtensionTypes(true, operation);
|
||||||
|
|
||||||
String url = getBaseUrl() + operation;
|
String url = getBaseUrl() + operation;
|
||||||
|
|
||||||
List<Resource> newOutputFiles = new ArrayList<>();
|
List<Resource> newOutputFiles = new ArrayList<>();
|
||||||
if (!isMultiInputOperation) {
|
if (!isMultiInputOperation) {
|
||||||
for (Resource file : outputFiles) {
|
for (Resource file : outputFiles) {
|
||||||
@@ -101,7 +112,6 @@ public class PipelineProcessor {
|
|||||||
hasInputFileType = true;
|
hasInputFileType = true;
|
||||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
if (entry.getValue() instanceof List) {
|
if (entry.getValue() instanceof List) {
|
||||||
List<?> list = (List<?>) entry.getValue();
|
List<?> list = (List<?>) entry.getValue();
|
||||||
@@ -112,9 +122,7 @@ public class PipelineProcessor {
|
|||||||
body.add(entry.getKey(), entry.getValue());
|
body.add(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
// If the operation is filter and the response body is null or empty,
|
// If the operation is filter and the response body is null or empty,
|
||||||
// skip
|
// skip
|
||||||
// this
|
// this
|
||||||
@@ -125,7 +133,6 @@ public class PipelineProcessor {
|
|||||||
log.info("Skipping file due to failing {}", operation);
|
log.info("Skipping file due to failing {}", operation);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.getStatusCode().equals(HttpStatus.OK)) {
|
if (!response.getStatusCode().equals(HttpStatus.OK)) {
|
||||||
logPrintStream.println("Error: " + response.getBody());
|
logPrintStream.println("Error: " + response.getBody());
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
@@ -134,7 +141,6 @@ public class PipelineProcessor {
|
|||||||
processOutputFiles(operation, response, newOutputFiles);
|
processOutputFiles(operation, response, newOutputFiles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasInputFileType) {
|
if (!hasInputFileType) {
|
||||||
logPrintStream.println(
|
logPrintStream.println(
|
||||||
"No files with extension "
|
"No files with extension "
|
||||||
@@ -144,7 +150,6 @@ public class PipelineProcessor {
|
|||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Filter and collect all files that match the inputFileExtension
|
// Filter and collect all files that match the inputFileExtension
|
||||||
List<Resource> matchingFiles;
|
List<Resource> matchingFiles;
|
||||||
@@ -160,17 +165,14 @@ public class PipelineProcessor {
|
|||||||
.anyMatch(file.getFilename()::endsWith))
|
.anyMatch(file.getFilename()::endsWith))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are matching files
|
// Check if there are matching files
|
||||||
if (!matchingFiles.isEmpty()) {
|
if (!matchingFiles.isEmpty()) {
|
||||||
// Create a new MultiValueMap for the request body
|
// Create a new MultiValueMap for the request body
|
||||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
|
|
||||||
// Add all matching files to the body
|
// Add all matching files to the body
|
||||||
for (Resource file : matchingFiles) {
|
for (Resource file : matchingFiles) {
|
||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
if (entry.getValue() instanceof List) {
|
if (entry.getValue() instanceof List) {
|
||||||
List<?> list = (List<?>) entry.getValue();
|
List<?> list = (List<?>) entry.getValue();
|
||||||
@@ -181,9 +183,7 @@ public class PipelineProcessor {
|
|||||||
body.add(entry.getKey(), entry.getValue());
|
body.add(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
// Handle the response
|
// Handle the response
|
||||||
if (response.getStatusCode().equals(HttpStatus.OK)) {
|
if (response.getStatusCode().equals(HttpStatus.OK)) {
|
||||||
processOutputFiles(operation, response, newOutputFiles);
|
processOutputFiles(operation, response, newOutputFiles);
|
||||||
@@ -208,48 +208,22 @@ public class PipelineProcessor {
|
|||||||
if (hasErrors) {
|
if (hasErrors) {
|
||||||
log.error("Errors occurred during processing. Log: {}", logStream.toString());
|
log.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return outputFiles;
|
return outputFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
|
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
|
||||||
// Set up headers, including API key
|
// Set up headers, including API key
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
String apiKey = getApiKeyForUser();
|
String apiKey = getApiKeyForUser();
|
||||||
headers.add("X-API-KEY", apiKey);
|
headers.add("X-API-KEY", apiKey);
|
||||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
|
||||||
// Create HttpEntity with the body and headers
|
// Create HttpEntity with the body and headers
|
||||||
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
// Make the request to the REST endpoint
|
// Make the request to the REST endpoint
|
||||||
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
|
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String removeTrailingNaming(String filename) {
|
|
||||||
// Splitting filename into name and extension
|
|
||||||
int dotIndex = filename.lastIndexOf(".");
|
|
||||||
if (dotIndex == -1) {
|
|
||||||
// No extension found
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
String name = filename.substring(0, dotIndex);
|
|
||||||
String extension = filename.substring(dotIndex);
|
|
||||||
|
|
||||||
// Finding the last underscore
|
|
||||||
int underscoreIndex = name.lastIndexOf("_");
|
|
||||||
if (underscoreIndex == -1) {
|
|
||||||
// No underscore found
|
|
||||||
return filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Removing the last part and reattaching the extension
|
|
||||||
return name.substring(0, underscoreIndex) + extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Resource> processOutputFiles(
|
private List<Resource> processOutputFiles(
|
||||||
String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles)
|
String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
@@ -259,13 +233,11 @@ public class PipelineProcessor {
|
|||||||
// If the operation is "auto-rename", generate a new filename.
|
// If the operation is "auto-rename", generate a new filename.
|
||||||
// This is a simple example of generating a filename using current timestamp.
|
// This is a simple example of generating a filename using current timestamp.
|
||||||
// Modify as per your needs.
|
// Modify as per your needs.
|
||||||
|
|
||||||
newFilename = extractFilename(response);
|
newFilename = extractFilename(response);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, keep the original filename.
|
// Otherwise, keep the original filename.
|
||||||
newFilename = removeTrailingNaming(extractFilename(response));
|
newFilename = removeTrailingNaming(extractFilename(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the response body is a zip file
|
// Check if the response body is a zip file
|
||||||
if (isZip(response.getBody())) {
|
if (isZip(response.getBody())) {
|
||||||
// Unzip the file and add all the files to the new output files
|
// Unzip the file and add all the files to the new output files
|
||||||
@@ -273,6 +245,7 @@ public class PipelineProcessor {
|
|||||||
} else {
|
} else {
|
||||||
Resource outputResource =
|
Resource outputResource =
|
||||||
new ByteArrayResource(response.getBody()) {
|
new ByteArrayResource(response.getBody()) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return newFilename;
|
return newFilename;
|
||||||
@@ -280,16 +253,14 @@ public class PipelineProcessor {
|
|||||||
};
|
};
|
||||||
newOutputFiles.add(outputResource);
|
newOutputFiles.add(outputResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newOutputFiles;
|
return newOutputFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String extractFilename(ResponseEntity<byte[]> response) {
|
public String extractFilename(ResponseEntity<byte[]> response) {
|
||||||
String filename = "default-filename.ext"; // Default filename if not found
|
// Default filename if not found
|
||||||
|
String filename = "default-filename.ext";
|
||||||
HttpHeaders headers = response.getHeaders();
|
HttpHeaders headers = response.getHeaders();
|
||||||
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
|
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
|
||||||
|
|
||||||
if (contentDisposition != null && !contentDisposition.isEmpty()) {
|
if (contentDisposition != null && !contentDisposition.isEmpty()) {
|
||||||
String[] parts = contentDisposition.split(";");
|
String[] parts = contentDisposition.split(";");
|
||||||
for (String part : parts) {
|
for (String part : parts) {
|
||||||
@@ -297,12 +268,10 @@ public class PipelineProcessor {
|
|||||||
// Extracts filename and removes quotes if present
|
// Extracts filename and removes quotes if present
|
||||||
filename = part.split("=")[1].trim().replace("\"", "");
|
filename = part.split("=")[1].trim().replace("\"", "");
|
||||||
filename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
|
filename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,16 +280,15 @@ public class PipelineProcessor {
|
|||||||
log.info("No files");
|
log.info("No files");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
Path path = Paths.get(file.getAbsolutePath());
|
Path path = Paths.get(file.getAbsolutePath());
|
||||||
log.info("Reading file: " + path); // debug statement
|
// debug statement
|
||||||
|
log.info("Reading file: " + path);
|
||||||
if (Files.exists(path)) {
|
if (Files.exists(path)) {
|
||||||
Resource fileResource =
|
Resource fileResource =
|
||||||
new ByteArrayResource(Files.readAllBytes(path)) {
|
new ByteArrayResource(Files.readAllBytes(path)) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return file.getName();
|
return file.getName();
|
||||||
@@ -340,12 +308,11 @@ public class PipelineProcessor {
|
|||||||
log.info("No files");
|
log.info("No files");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Resource> outputFiles = new ArrayList<>();
|
List<Resource> outputFiles = new ArrayList<>();
|
||||||
|
|
||||||
for (MultipartFile file : files) {
|
for (MultipartFile file : files) {
|
||||||
Resource fileResource =
|
Resource fileResource =
|
||||||
new ByteArrayResource(file.getBytes()) {
|
new ByteArrayResource(file.getBytes()) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return Filenames.toSimpleFileName(file.getOriginalFilename());
|
return Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||||
@@ -361,7 +328,6 @@ public class PipelineProcessor {
|
|||||||
if (data == null || data.length < 4) {
|
if (data == null || data.length < 4) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the first four bytes of the data against the standard zip magic number
|
// Check the first four bytes of the data against the standard zip magic number
|
||||||
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
|
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
|
||||||
}
|
}
|
||||||
@@ -369,29 +335,25 @@ public class PipelineProcessor {
|
|||||||
private List<Resource> unzip(byte[] data) throws IOException {
|
private List<Resource> unzip(byte[] data) throws IOException {
|
||||||
log.info("Unzipping data of length: {}", data.length);
|
log.info("Unzipping data of length: {}", data.length);
|
||||||
List<Resource> unzippedFiles = new ArrayList<>();
|
List<Resource> unzippedFiles = new ArrayList<>();
|
||||||
|
|
||||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
|
||||||
ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) {
|
ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) {
|
||||||
|
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
byte[] buffer = new byte[1024];
|
byte[] buffer = new byte[1024];
|
||||||
int count;
|
int count;
|
||||||
|
|
||||||
while ((count = zis.read(buffer)) != -1) {
|
while ((count = zis.read(buffer)) != -1) {
|
||||||
baos.write(buffer, 0, count);
|
baos.write(buffer, 0, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String filename = entry.getName();
|
final String filename = entry.getName();
|
||||||
Resource fileResource =
|
Resource fileResource =
|
||||||
new ByteArrayResource(baos.toByteArray()) {
|
new ByteArrayResource(baos.toByteArray()) {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the unzipped file is a zip file, unzip it
|
// If the unzipped file is a zip file, unzip it
|
||||||
if (isZip(baos.toByteArray())) {
|
if (isZip(baos.toByteArray())) {
|
||||||
log.info("File {} is a zip file. Unzipping...", filename);
|
log.info("File {} is a zip file. Unzipping...", filename);
|
||||||
@@ -401,7 +363,6 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
|
||||||
return unzippedFiles;
|
return unzippedFiles;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.security.KeyStore;
|
import java.security.*;
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.UnrecoverableKeyException;
|
|
||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
@@ -91,6 +80,151 @@ public class CertSignController {
|
|||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sign(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
|
byte[] input,
|
||||||
|
OutputStream output,
|
||||||
|
CreateSignature instance,
|
||||||
|
Boolean showSignature,
|
||||||
|
Integer pageNumber,
|
||||||
|
String name,
|
||||||
|
String location,
|
||||||
|
String reason,
|
||||||
|
Boolean showLogo) {
|
||||||
|
try (PDDocument doc = pdfDocumentFactory.load(input)) {
|
||||||
|
PDSignature signature = new PDSignature();
|
||||||
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||||
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||||
|
signature.setName(name);
|
||||||
|
signature.setLocation(location);
|
||||||
|
signature.setReason(reason);
|
||||||
|
signature.setSignDate(Calendar.getInstance());
|
||||||
|
|
||||||
|
if (showSignature) {
|
||||||
|
SignatureOptions signatureOptions = new SignatureOptions();
|
||||||
|
signatureOptions.setVisualSignature(
|
||||||
|
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
|
||||||
|
signatureOptions.setPage(pageNumber);
|
||||||
|
|
||||||
|
doc.addSignature(signature, instance, signatureOptions);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
doc.addSignature(signature, instance);
|
||||||
|
}
|
||||||
|
doc.saveIncremental(output);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("exception", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
||||||
|
@Operation(
|
||||||
|
summary = "Sign PDF with a Digital Certificate",
|
||||||
|
description =
|
||||||
|
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
|
||||||
|
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
|
||||||
|
+ " Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile pdf = request.getFileInput();
|
||||||
|
String certType = request.getCertType();
|
||||||
|
MultipartFile privateKeyFile = request.getPrivateKeyFile();
|
||||||
|
MultipartFile certFile = request.getCertFile();
|
||||||
|
MultipartFile p12File = request.getP12File();
|
||||||
|
MultipartFile jksfile = request.getJksFile();
|
||||||
|
String password = request.getPassword();
|
||||||
|
Boolean showSignature = request.isShowSignature();
|
||||||
|
String reason = request.getReason();
|
||||||
|
String location = request.getLocation();
|
||||||
|
String name = request.getName();
|
||||||
|
Integer pageNumber = request.getPageNumber() - 1;
|
||||||
|
Boolean showLogo = request.isShowLogo();
|
||||||
|
|
||||||
|
if (certType == null) {
|
||||||
|
throw new IllegalArgumentException("Cert type must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStore ks = null;
|
||||||
|
|
||||||
|
switch (certType) {
|
||||||
|
case "PEM":
|
||||||
|
ks = KeyStore.getInstance("JKS");
|
||||||
|
ks.load(null);
|
||||||
|
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
|
||||||
|
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
|
||||||
|
ks.setKeyEntry(
|
||||||
|
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
|
||||||
|
break;
|
||||||
|
case "PKCS12":
|
||||||
|
ks = KeyStore.getInstance("PKCS12");
|
||||||
|
ks.load(p12File.getInputStream(), password.toCharArray());
|
||||||
|
break;
|
||||||
|
case "JKS":
|
||||||
|
ks = KeyStore.getInstance("JKS");
|
||||||
|
ks.load(jksfile.getInputStream(), password.toCharArray());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
sign(
|
||||||
|
pdfDocumentFactory,
|
||||||
|
pdf.getBytes(),
|
||||||
|
baos,
|
||||||
|
createSignature,
|
||||||
|
showSignature,
|
||||||
|
pageNumber,
|
||||||
|
name,
|
||||||
|
location,
|
||||||
|
reason,
|
||||||
|
showLogo);
|
||||||
|
return WebResponseUtils.boasToWebResponse(
|
||||||
|
baos,
|
||||||
|
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_signed.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
|
||||||
|
throws IOException, OperatorCreationException, PKCSException {
|
||||||
|
try (PEMParser pemParser =
|
||||||
|
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
||||||
|
Object pemObject = pemParser.readObject();
|
||||||
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||||
|
PrivateKeyInfo pkInfo;
|
||||||
|
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
|
||||||
|
InputDecryptorProvider decProv =
|
||||||
|
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
||||||
|
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
|
||||||
|
} else if (pemObject instanceof PEMEncryptedKeyPair) {
|
||||||
|
PEMDecryptorProvider decProv =
|
||||||
|
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||||
|
pkInfo =
|
||||||
|
((PEMEncryptedKeyPair) pemObject)
|
||||||
|
.decryptKeyPair(decProv)
|
||||||
|
.getPrivateKeyInfo();
|
||||||
|
} else {
|
||||||
|
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
||||||
|
}
|
||||||
|
return converter.getPrivateKey(pkInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Certificate getCertificateFromPEM(byte[] pemBytes)
|
||||||
|
throws IOException, CertificateException {
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) {
|
||||||
|
return CertificateFactory.getInstance("X.509").generateCertificate(bis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CreateSignature extends CreateSignatureBase {
|
class CreateSignature extends CreateSignatureBase {
|
||||||
File logoFile;
|
File logoFile;
|
||||||
|
|
||||||
@@ -198,149 +332,4 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
|
|
||||||
@Operation(
|
|
||||||
summary = "Sign PDF with a Digital Certificate",
|
|
||||||
description =
|
|
||||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
|
|
||||||
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
|
|
||||||
+ " Type:SISO")
|
|
||||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
|
||||||
throws Exception {
|
|
||||||
MultipartFile pdf = request.getFileInput();
|
|
||||||
String certType = request.getCertType();
|
|
||||||
MultipartFile privateKeyFile = request.getPrivateKeyFile();
|
|
||||||
MultipartFile certFile = request.getCertFile();
|
|
||||||
MultipartFile p12File = request.getP12File();
|
|
||||||
MultipartFile jksfile = request.getJksFile();
|
|
||||||
String password = request.getPassword();
|
|
||||||
Boolean showSignature = request.isShowSignature();
|
|
||||||
String reason = request.getReason();
|
|
||||||
String location = request.getLocation();
|
|
||||||
String name = request.getName();
|
|
||||||
Integer pageNumber = request.getPageNumber() - 1;
|
|
||||||
Boolean showLogo = request.isShowLogo();
|
|
||||||
|
|
||||||
if (certType == null) {
|
|
||||||
throw new IllegalArgumentException("Cert type must be provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyStore ks = null;
|
|
||||||
|
|
||||||
switch (certType) {
|
|
||||||
case "PEM":
|
|
||||||
ks = KeyStore.getInstance("JKS");
|
|
||||||
ks.load(null);
|
|
||||||
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
|
|
||||||
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
|
|
||||||
ks.setKeyEntry(
|
|
||||||
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
|
|
||||||
break;
|
|
||||||
case "PKCS12":
|
|
||||||
ks = KeyStore.getInstance("PKCS12");
|
|
||||||
ks.load(p12File.getInputStream(), password.toCharArray());
|
|
||||||
break;
|
|
||||||
case "JKS":
|
|
||||||
ks = KeyStore.getInstance("JKS");
|
|
||||||
ks.load(jksfile.getInputStream(), password.toCharArray());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
sign(
|
|
||||||
pdfDocumentFactory,
|
|
||||||
pdf.getBytes(),
|
|
||||||
baos,
|
|
||||||
createSignature,
|
|
||||||
showSignature,
|
|
||||||
pageNumber,
|
|
||||||
name,
|
|
||||||
location,
|
|
||||||
reason,
|
|
||||||
showLogo);
|
|
||||||
return WebResponseUtils.boasToWebResponse(
|
|
||||||
baos,
|
|
||||||
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_signed.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void sign(
|
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
|
||||||
byte[] input,
|
|
||||||
OutputStream output,
|
|
||||||
CreateSignature instance,
|
|
||||||
Boolean showSignature,
|
|
||||||
Integer pageNumber,
|
|
||||||
String name,
|
|
||||||
String location,
|
|
||||||
String reason,
|
|
||||||
Boolean showLogo) {
|
|
||||||
try (PDDocument doc = pdfDocumentFactory.load(input)) {
|
|
||||||
PDSignature signature = new PDSignature();
|
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
|
||||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
|
||||||
signature.setName(name);
|
|
||||||
signature.setLocation(location);
|
|
||||||
signature.setReason(reason);
|
|
||||||
signature.setSignDate(Calendar.getInstance());
|
|
||||||
|
|
||||||
if (showSignature) {
|
|
||||||
SignatureOptions signatureOptions = new SignatureOptions();
|
|
||||||
signatureOptions.setVisualSignature(
|
|
||||||
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
|
|
||||||
signatureOptions.setPage(pageNumber);
|
|
||||||
|
|
||||||
doc.addSignature(signature, instance, signatureOptions);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
doc.addSignature(signature, instance);
|
|
||||||
}
|
|
||||||
doc.saveIncremental(output);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("exception", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
|
|
||||||
throws IOException, OperatorCreationException, PKCSException {
|
|
||||||
try (PEMParser pemParser =
|
|
||||||
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
|
|
||||||
Object pemObject = pemParser.readObject();
|
|
||||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
|
||||||
PrivateKeyInfo pkInfo;
|
|
||||||
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
|
|
||||||
InputDecryptorProvider decProv =
|
|
||||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
|
||||||
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
|
|
||||||
} else if (pemObject instanceof PEMEncryptedKeyPair) {
|
|
||||||
PEMDecryptorProvider decProv =
|
|
||||||
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
|
||||||
pkInfo =
|
|
||||||
((PEMEncryptedKeyPair) pemObject)
|
|
||||||
.decryptKeyPair(decProv)
|
|
||||||
.getPrivateKeyInfo();
|
|
||||||
} else {
|
|
||||||
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
|
||||||
}
|
|
||||||
return converter.getPrivateKey(pkInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Certificate getCertificateFromPEM(byte[] pemBytes)
|
|
||||||
throws IOException, CertificateException {
|
|
||||||
try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) {
|
|
||||||
return CertificateFactory.getInstance("X.509").generateCertificate(bis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,13 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Calendar;
|
import java.util.*;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.cos.COSInputStream;
|
import org.apache.pdfbox.cos.COSInputStream;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.cos.COSString;
|
import org.apache.pdfbox.cos.COSString;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.*;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||||
@@ -83,6 +71,48 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
static ObjectMapper objectMapper = new ObjectMapper();
|
static ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||||
|
if (outline == null) return;
|
||||||
|
|
||||||
|
ObjectNode outlineNode = objectMapper.createObjectNode();
|
||||||
|
outlineNode.put("Title", outline.getTitle());
|
||||||
|
// You can add other properties if needed
|
||||||
|
arrayNode.add(outlineNode);
|
||||||
|
|
||||||
|
PDOutlineItem child = outline.getFirstChild();
|
||||||
|
while (child != null) {
|
||||||
|
addOutlinesToArray(child, arrayNode);
|
||||||
|
child = child.getNextSibling();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
||||||
|
// Check XMP Metadata
|
||||||
|
try {
|
||||||
|
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
||||||
|
if (pdMetadata != null) {
|
||||||
|
COSInputStream metaStream = pdMetadata.createInputStream();
|
||||||
|
DomXmpParser domXmpParser = new DomXmpParser();
|
||||||
|
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
new XmpSerializer().serialize(xmpMeta, baos, true);
|
||||||
|
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if (xmpString.contains(standardKeyword)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (
|
||||||
|
Exception
|
||||||
|
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||||
|
// exceptions.
|
||||||
|
log.error("exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||||
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
|
||||||
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
|
||||||
@@ -606,21 +636,6 @@ public class GetInfoOnPDF {
|
|||||||
return state ? "Allowed" : "Not Allowed";
|
return state ? "Allowed" : "Not Allowed";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
|
||||||
if (outline == null) return;
|
|
||||||
|
|
||||||
ObjectNode outlineNode = objectMapper.createObjectNode();
|
|
||||||
outlineNode.put("Title", outline.getTitle());
|
|
||||||
// You can add other properties if needed
|
|
||||||
arrayNode.add(outlineNode);
|
|
||||||
|
|
||||||
PDOutlineItem child = outline.getFirstChild();
|
|
||||||
while (child != null) {
|
|
||||||
addOutlinesToArray(child, arrayNode);
|
|
||||||
child = child.getNextSibling();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPageOrientation(double width, double height) {
|
public String getPageOrientation(double width, double height) {
|
||||||
if (width > height) {
|
if (width > height) {
|
||||||
return "Landscape";
|
return "Landscape";
|
||||||
@@ -678,33 +693,6 @@ public class GetInfoOnPDF {
|
|||||||
return dimensionInfo;
|
return dimensionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
|
|
||||||
// Check XMP Metadata
|
|
||||||
try {
|
|
||||||
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
|
|
||||||
if (pdMetadata != null) {
|
|
||||||
COSInputStream metaStream = pdMetadata.createInputStream();
|
|
||||||
DomXmpParser domXmpParser = new DomXmpParser();
|
|
||||||
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
new XmpSerializer().serialize(xmpMeta, baos, true);
|
|
||||||
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
if (xmpString.contains(standardKeyword)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (
|
|
||||||
Exception
|
|
||||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
|
||||||
// exceptions.
|
|
||||||
log.error("exception", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
public ArrayNode exploreStructureTree(List<Object> nodes) {
|
||||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||||
if (nodes != null) {
|
if (nodes != null) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -4,17 +4,9 @@ import java.io.IOException;
|
|||||||
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
import org.apache.pdfbox.cos.COSDictionary;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.*;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
import org.apache.pdfbox.pdmodel.common.PDMetadata;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
|
import org.apache.pdfbox.pdmodel.interactive.action.*;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
import org.bouncycastle.cert.X509CertificateHolder;
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
import org.bouncycastle.cms.CMSProcessable;
|
import org.bouncycastle.cms.*;
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
|
||||||
import org.bouncycastle.cms.CMSSignedData;
|
|
||||||
import org.bouncycastle.cms.SignerInformation;
|
|
||||||
import org.bouncycastle.cms.SignerInformationStore;
|
|
||||||
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||||
import org.bouncycastle.util.Store;
|
import org.bouncycastle.util.Store;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user