Compare commits
151 commits
main
...
dgn-v0.28.
Author | SHA1 | Date | |
---|---|---|---|
f756a0f47e | |||
|
d75e4f1318 | ||
|
4088132597 | ||
|
7b9d419267 | ||
|
9b710b489e | ||
|
e2ff418c89 | ||
|
be006f08a6 | ||
|
b007c805bf | ||
|
24be10eed4 | ||
|
0854a1d26e | ||
|
33c7bb7e13 | ||
|
cdf31622e2 | ||
|
c7e5987342 | ||
|
d3ef335c24 | ||
|
b23784f598 | ||
|
90cbcde029 | ||
|
382edc01f8 | ||
|
dee912f075 | ||
|
faed367cde | ||
|
788744c1be | ||
|
686b88d21d | ||
|
1a594b27ab | ||
|
9f0088c839 | ||
|
5564a6e730 | ||
|
8a58647ffd | ||
|
37dcae282a | ||
|
58618b3a21 | ||
|
4a4c7faf47 | ||
|
66324a5bdc | ||
|
3bd18f7c5e | ||
|
e693bbb2bd | ||
|
f11ad92fa5 | ||
|
f443a4e0de | ||
|
fa0152aa2d | ||
|
e1d0f2cd3e | ||
|
81e2a77e57 | ||
|
6c9a4e8acc | ||
|
8e5b3ea7f1 | ||
|
b47f8a2c17 | ||
|
56a07bbf3a | ||
|
987d793ef4 | ||
|
ea85d76f5b | ||
|
fc762329a8 | ||
|
48bae227f6 | ||
|
9773138612 | ||
|
1927801894 | ||
|
3cd6f462a8 | ||
|
6dab3980fc | ||
|
ea2d755808 | ||
|
05efcedea8 | ||
|
29fcbf30d7 | ||
|
2cbe34ea24 | ||
|
29f43c010e | ||
|
8602f38fbf | ||
|
f5258c593b | ||
|
e89ac84928 | ||
|
8997855922 | ||
|
09c93cebe3 | ||
|
00d65596d1 | ||
|
851b43fadf | ||
|
d5ac560452 | ||
|
4ea323b879 | ||
|
5c84ae1b53 | ||
|
96a8898c15 | ||
|
1cca13334e | ||
|
264763dd4c | ||
|
eafbfb8dbf | ||
|
6fa7c2e5e1 | ||
|
909054a49d | ||
|
711501a382 | ||
|
e45b512087 | ||
|
d32da95f55 | ||
|
b54d73d723 | ||
|
503a1c9526 | ||
|
f176558a39 | ||
|
68c387086c | ||
|
f165439d26 | ||
|
6649ffd7a0 | ||
|
8dbbacb09e | ||
|
908b409155 | ||
|
4ad716f281 | ||
|
148feda83f | ||
|
771b312ee8 | ||
|
00a0670954 | ||
|
39423c247c | ||
|
6d8d0bad56 | ||
|
a3374745f8 | ||
|
d65a637a46 | ||
|
d0bf385d69 | ||
|
bc35745768 | ||
|
e50391a44a | ||
|
96b080528b | ||
|
f35cbc4310 | ||
|
c09fc1541f | ||
|
dff53310a7 | ||
|
ec537c6fde | ||
|
ce70796fff | ||
|
7db7192d95 | ||
|
d00e7fe958 | ||
|
510f39ad41 | ||
|
950a0c4b21 | ||
|
e6793bd04a | ||
|
0f60974a57 | ||
|
0ed4c16dc0 | ||
|
ea6d4a293e | ||
|
191e79da18 | ||
|
c54c18b247 | ||
|
39cbb5e7d9 | ||
|
3df0474ed2 | ||
|
9ff2cb63d0 | ||
|
d8087d8c55 | ||
|
0dfb4d77c0 | ||
|
065f53e577 | ||
|
c899f605a9 | ||
|
47de0f84db | ||
|
543b96c033 | ||
|
c1126e57bd | ||
|
7c5077006d | ||
|
3e7889cee8 | ||
|
281047f42a | ||
|
07f85ea8b4 | ||
|
e07f73dce7 | ||
|
bfe38c71e8 | ||
|
072090d41b | ||
|
560936e182 | ||
|
6eb79e65fa | ||
|
cbe92269f4 | ||
|
81871a6f10 | ||
|
cf2a7896da | ||
|
6a3d95ba09 | ||
|
85ed0c38d1 | ||
|
6c7dc34640 | ||
|
ecfdfa5644 | ||
|
11e279bd12 | ||
|
929f0bbbe5 | ||
|
5751b1ac2d | ||
|
4bf78ffd5d | ||
|
b7d37deb85 | ||
|
2a65fd0825 | ||
|
422264a288 | ||
|
695fbb0150 | ||
|
a17105e650 | ||
|
32ac38e93f | ||
|
3c0d2b908f | ||
|
ab62a93a0d | ||
|
5189708d25 | ||
|
19831c050c | ||
|
e426d99145 | ||
|
4088208fc8 | ||
|
31ce5b1221 | ||
|
be05db22f5 |
236 changed files with 12057 additions and 1596 deletions
12
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
12
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
|
@ -11,6 +11,18 @@ body:
|
||||||
|
|
||||||
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: installation-method
|
||||||
|
attributes:
|
||||||
|
label: Installation Method
|
||||||
|
description: |
|
||||||
|
Indicate whether you are using Docker or a local installation.
|
||||||
|
options:
|
||||||
|
- Docker
|
||||||
|
- Docker ultra lite
|
||||||
|
- Docker fat
|
||||||
|
- Local Installation
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
validations:
|
validations:
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
|
@ -1,6 +1,8 @@
|
||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Submit a new feature request.
|
description: Submit a new feature request.
|
||||||
title: "[Feature Request]: "
|
title: "[Feature Request]: "
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
|
@ -9,6 +9,8 @@ updates:
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
rebase-strategy: "auto"
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/" # Location of Dockerfile
|
directory: "/" # Location of Dockerfile
|
||||||
schedule:
|
schedule:
|
||||||
|
|
54
.github/labeler-config.yml
vendored
Normal file
54
.github/labeler-config.yml
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
Translation:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
||||||
|
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
|
||||||
|
|
||||||
|
Front End:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/static/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
||||||
|
|
||||||
|
Java:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
||||||
|
|
||||||
|
Back End:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
|
||||||
|
|
||||||
|
API:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: '**/*.md'
|
||||||
|
- any-glob-to-any-file: 'scripts/counter_translation.py'
|
||||||
|
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||||
|
|
||||||
|
Docker:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'Dockerfile'
|
||||||
|
- any-glob-to-any-file: 'Dockerfile-*'
|
||||||
|
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
||||||
|
|
||||||
|
Test:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'cucumber/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/test**/*'
|
||||||
|
|
||||||
|
Github:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: '.github/**/*'
|
93
.github/labels.yml
vendored
Normal file
93
.github/labels.yml
vendored
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
# Labels names are important as they are used by Release Drafter to decide
|
||||||
|
# regarding where to record them in changelog or if to skip them.
|
||||||
|
#
|
||||||
|
# The repository labels will be automatically configured using this file and
|
||||||
|
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
||||||
|
- name: "Back End"
|
||||||
|
color: "20CE6C"
|
||||||
|
description: "Issues related to back-end development"
|
||||||
|
from_name: "Back end"
|
||||||
|
- name: "Bug"
|
||||||
|
description: "Something isn't working"
|
||||||
|
color: "EB9CA6"
|
||||||
|
from_name: "bug"
|
||||||
|
- name: "dependencies"
|
||||||
|
description: "Pull requests that update a dependency file"
|
||||||
|
color: "5AA8FC"
|
||||||
|
- name: "Docker"
|
||||||
|
description: "Pull requests that update Docker code"
|
||||||
|
color: "1FCEFF"
|
||||||
|
from_name: "docker"
|
||||||
|
- name: "Documentation"
|
||||||
|
description: "Improvements or additions to documentation"
|
||||||
|
color: "35ABFF"
|
||||||
|
from_name: "documentation"
|
||||||
|
- name: "Done for next release"
|
||||||
|
color: "0CDBD1"
|
||||||
|
- name: "Done"
|
||||||
|
color: "60F13B"
|
||||||
|
- name: "duplicate"
|
||||||
|
description: "This issue or pull request already exists"
|
||||||
|
color: "CDD1D5"
|
||||||
|
- name: "enhancement"
|
||||||
|
description: "New feature or request"
|
||||||
|
color: "A0EEEE"
|
||||||
|
- name: "fix needs confirmation"
|
||||||
|
color: "60A1E7"
|
||||||
|
description: "Fix needs to be confirmed"
|
||||||
|
- name: "Front End"
|
||||||
|
color: "BBD2F1"
|
||||||
|
description: "Issues related to front-end development"
|
||||||
|
- name: "github-actions"
|
||||||
|
description: "Pull requests that update GitHub Actions code"
|
||||||
|
color: "999999"
|
||||||
|
from_name: "github_actions"
|
||||||
|
- name: "good first issue"
|
||||||
|
description: "Good for newcomers"
|
||||||
|
color: "C1B8FF"
|
||||||
|
- name: "help wanted"
|
||||||
|
description: "Extra attention is needed"
|
||||||
|
color: "00E6C4"
|
||||||
|
- name: "invalid"
|
||||||
|
description: "This doesn't seem right"
|
||||||
|
color: "E5E566"
|
||||||
|
- name: "Java"
|
||||||
|
description: "Pull requests that update Java code"
|
||||||
|
color: "FF9E1F"
|
||||||
|
from_name: "java"
|
||||||
|
- name: "Long-term Enhancement"
|
||||||
|
color: "BFDEC3"
|
||||||
|
description: "Enhancements planned for the long term"
|
||||||
|
- name: "more-info-needed"
|
||||||
|
color: "00E4F8"
|
||||||
|
description: "More information is needed"
|
||||||
|
- name: "needs investigation"
|
||||||
|
color: "B8C3A7"
|
||||||
|
description: "Issues that require further investigation"
|
||||||
|
- name: "Prioritised enhancement"
|
||||||
|
color: "4BA2EE"
|
||||||
|
description: "High-priority enhancements"
|
||||||
|
- name: "question"
|
||||||
|
description: "Further information is requested"
|
||||||
|
color: "D97EE5"
|
||||||
|
- name: "Translation"
|
||||||
|
color: "9FABF9"
|
||||||
|
from_name: "translation"
|
||||||
|
- name: "upstream"
|
||||||
|
color: "DEDEDE"
|
||||||
|
- name: "v2"
|
||||||
|
color: "FFFF00"
|
||||||
|
- name: "wontfix"
|
||||||
|
description: "This will not be worked on"
|
||||||
|
color: "FFFFFF"
|
||||||
|
- name: "Security"
|
||||||
|
color: "000000"
|
||||||
|
description: "Security-related issues or pull requests"
|
||||||
|
- name: "API"
|
||||||
|
color: "FFFF00"
|
||||||
|
description: "API-related issues or pull requests"
|
||||||
|
- name: "Test"
|
||||||
|
color: "FF9E1F"
|
||||||
|
description: "Testing-related issues or pull requests"
|
||||||
|
- name: "Stale"
|
||||||
|
color: "000000"
|
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
|
@ -10,9 +10,3 @@ Closes #(issue_number)
|
||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
|
|
||||||
## Contributor License Agreement
|
|
||||||
|
|
||||||
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
|
||||||
|
|
||||||
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
|
||||||
|
|
32
.github/release.yml
vendored
Normal file
32
.github/release.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- Documentation
|
||||||
|
- Test
|
||||||
|
- Github
|
||||||
|
|
||||||
|
categories:
|
||||||
|
- title: Bug Fixes
|
||||||
|
labels:
|
||||||
|
- Bug
|
||||||
|
|
||||||
|
- title: Enhancements
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
|
||||||
|
- title: Minor Enhancements
|
||||||
|
labels:
|
||||||
|
- Java
|
||||||
|
- Front End
|
||||||
|
|
||||||
|
- title: Docker Updates
|
||||||
|
labels:
|
||||||
|
- Docker
|
||||||
|
|
||||||
|
- title: Translation Changes
|
||||||
|
labels:
|
||||||
|
- Translation
|
||||||
|
|
||||||
|
- title: Other Changes
|
||||||
|
labels:
|
||||||
|
- "*"
|
1
.github/scripts/check_tabulator.py
vendored
1
.github/scripts/check_tabulator.py
vendored
|
@ -1,4 +1,5 @@
|
||||||
"""check_tabulator.py"""
|
"""check_tabulator.py"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
20
.github/workflows/auto-labeler.yml
vendored
Normal file
20
.github/workflows/auto-labeler.yml
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
name: "Pull Request Labeler"
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
labeler:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Apply Labels
|
||||||
|
uses: actions/labeler@v5
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
configuration-path: .github/labeler-config.yml
|
||||||
|
sync-labels: true
|
60
.github/workflows/build.yml
vendored
60
.github/workflows/build.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: "Build repo"
|
name: Build repo
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
@ -17,20 +17,72 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
jdk-version: [17, 21]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: ${{ matrix.jdk-version }}
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --no-build-cache
|
run: ./gradlew build --no-build-cache
|
||||||
|
|
||||||
|
docker-compose-tests:
|
||||||
|
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||||
|
# (github.event_name == 'pull_request' &&
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
|
||||||
|
# (
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Java') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Security') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'API') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Test')
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "adopt"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install Docker Compose
|
||||||
|
run: |
|
||||||
|
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.7"
|
||||||
|
|
||||||
|
- name: Pip requirements
|
||||||
|
run: |
|
||||||
|
pip install -r ./cucumber/requirements.txt
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
|
run: |
|
||||||
|
chmod +x ./test.sh
|
||||||
|
./test.sh
|
||||||
|
|
19
.github/workflows/licenses-update.yml
vendored
19
.github/workflows/licenses-update.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean generateLicenseReport
|
run: ./gradlew clean generateLicenseReport
|
||||||
|
@ -45,6 +45,7 @@ jobs:
|
||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
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@v6
|
||||||
with:
|
with:
|
||||||
|
@ -57,6 +58,22 @@ jobs:
|
||||||
title: "Update 3rd Party Licenses"
|
title: "Update 3rd Party Licenses"
|
||||||
body: |
|
body: |
|
||||||
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
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
|
||||||
|
- name: Auto approve
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
run: gh pr review --approve "${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Enable auto-merge
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
merge-method: squash # Choose the merge method: merge, squash, or rebase
|
||||||
|
|
24
.github/workflows/manage-label.yml
vendored
Normal file
24
.github/workflows/manage-label.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
name: Manage labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 20 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
labeler:
|
||||||
|
name: Labeler
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Labeler
|
||||||
|
uses: crazy-max/ghaction-github-labeler@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
yaml-file: .github/labels.yml
|
||||||
|
skip-delete: true
|
9
.github/workflows/push-docker.yml
vendored
9
.github/workflows/push-docker.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ 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@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
|
@ -98,7 +98,7 @@ 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@v5
|
uses: docker/build-push-action@v6
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
@ -111,7 +111,6 @@ jobs:
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
|
@ -125,7 +124,7 @@ 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@v5
|
uses: docker/build-push-action@v6
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
|
2
.github/workflows/releaseArtifacts.yml
vendored
2
.github/workflows/releaseArtifacts.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
|
|
32
.github/workflows/stale.yml
vendored
Normal file
32
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
name: Close stale issues
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 0 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: 30 days stale issues
|
||||||
|
uses: actions/stale@v9
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
days-before-stale: 30
|
||||||
|
days-before-close: 7
|
||||||
|
stale-issue-message: >
|
||||||
|
This issue has been automatically marked as stale because it has had no recent activity.
|
||||||
|
It will be closed if no further activity occurs. Thank you for your contributions.
|
||||||
|
close-issue-message: >
|
||||||
|
This issue has been automatically closed because it has had no recent activity after being marked as stale.
|
||||||
|
Please reopen if you need further assistance.
|
||||||
|
stale-issue-label: "Stale"
|
||||||
|
remove-stale-when-updated: true
|
||||||
|
only-issue-labels: "more-info-needed"
|
||||||
|
days-before-pr-stale: -1 # Prevents PRs from being marked as stale
|
||||||
|
days-before-pr-close: -1 # Prevents PRs from being closed
|
||||||
|
start-date: '2024-07-06T00:00:00Z' # ISO 8601 Format
|
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
|
@ -18,7 +18,7 @@ jobs:
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|
2
.github/workflows/sync_files.yml
vendored
2
.github/workflows/sync_files.yml
vendored
|
@ -51,6 +51,7 @@ jobs:
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
labels: github-actions
|
||||||
sync-readme:
|
sync-readme:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
@ -88,3 +89,4 @@ jobs:
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
labels: Documentation,Translation,github-actions
|
||||||
|
|
47
.github/workflows/test.yml
vendored
47
.github/workflows/test.yml
vendored
|
@ -1,47 +0,0 @@
|
||||||
name: Docker Compose Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "src/**"
|
|
||||||
- "**.gradle"
|
|
||||||
- "!src/main/java/resources/messages*"
|
|
||||||
- "exampleYmlFiles/**"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "Dockerfile**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Java 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "adopt"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Install Docker Compose
|
|
||||||
run: |
|
|
||||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
# sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.7"
|
|
||||||
|
|
||||||
- name: Pip requirements
|
|
||||||
run: |
|
|
||||||
pip install -r ./cucumber/requirements.txt
|
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
|
||||||
run: |
|
|
||||||
chmod +x ./test.sh
|
|
||||||
./test.sh
|
|
45
.gitignore
vendored
45
.gitignore
vendored
|
@ -1,5 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
### Eclipse ###
|
### Eclipse ###
|
||||||
.metadata
|
.metadata
|
||||||
bin/
|
bin/
|
||||||
|
@ -22,7 +20,6 @@ customFiles/
|
||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
.lock
|
.lock
|
||||||
|
@ -119,12 +116,48 @@ watchedFolders/
|
||||||
*.db
|
*.db
|
||||||
/build
|
/build
|
||||||
|
|
||||||
/.vscode
|
# Byte-compiled / optimized / DLL files
|
||||||
/.idea
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env*
|
||||||
|
.venv*
|
||||||
|
env*/
|
||||||
|
venv*/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
/.vscode/**/*
|
||||||
|
!/.vscode/settings.json
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
out/
|
||||||
|
|
||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
#cucumber
|
# cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
|
# Certs
|
||||||
|
*.p12
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.cer
|
||||||
|
*.der
|
||||||
|
*.key
|
||||||
|
*.csr
|
||||||
|
|
||||||
|
# cache
|
||||||
|
.ruff_cache
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
|
53
.vscode/settings.json
vendored
Normal file
53
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"files.eol": "auto",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
|
"black-formatter.args": ["--line-length", "127"],
|
||||||
|
"flake8.args": ["--max-line-length", "127"],
|
||||||
|
"pylint.args": ["max-line-length", "127"],
|
||||||
|
"[java]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[gradle-build]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[gradle]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.rulers": [127],
|
||||||
|
"files.trimFinalNewlines": false,
|
||||||
|
"files.insertFinalNewline": false
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"files.trimFinalNewlines": false,
|
||||||
|
"files.insertFinalNewline": false
|
||||||
|
},
|
||||||
|
"diffEditor.maxComputationTime": 0,
|
||||||
|
"editor.wordSegmenterLocales": null,
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimFinalNewlines": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"editor.indentSize": "tabSize",
|
||||||
|
"editor.stickyScroll.enabled": false,
|
||||||
|
"editor.minimap.enabled": false,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
40
DATABASE.md
Normal file
40
DATABASE.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# New Database Backup and Import Functionality
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
1. Automatic Backup Creation
|
||||||
|
- The system automatically creates a database backup every day at midnight. This ensures that there is always a recent backup available, minimizing the risk of data loss.
|
||||||
|
2. Manual Backup Export
|
||||||
|
- Admin actions that modify the user database trigger a manual export of the database. This keeps the backup up-to-date with the latest changes and provides an extra layer of data security.
|
||||||
|
3. Importing Database Backups
|
||||||
|
- Admin users can import a database backup either via the web interface or API endpoints. This allows for easy restoration of the database to a previous state in case of data corruption or other issues.
|
||||||
|
- The import process ensures that the database structure and data are correctly restored, maintaining the integrity of the application.
|
||||||
|
4. Managing Backup Files
|
||||||
|
- Admins can view a list of all existing backup files, along with their creation dates and sizes. This helps in managing storage and identifying the most recent or relevant backups.
|
||||||
|
- Backup files can be downloaded for offline storage or transferred to other environments, providing flexibility in database management.
|
||||||
|
- Unnecessary backup files can be deleted through the interface to free up storage space and maintain an organized backup directory.
|
||||||
|
|
||||||
|
## User Interface
|
||||||
|
|
||||||
|
### Web Interface
|
||||||
|
|
||||||
|
1. Upload SQL files to import database backups.
|
||||||
|
2. View details of existing backups, such as file names, creation dates, and sizes.
|
||||||
|
3. Download backup files for offline storage.
|
||||||
|
4. Delete outdated or unnecessary backup files.
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
1. Import database backups by uploading SQL files.
|
||||||
|
2. Download backup files.
|
||||||
|
3. Delete backup files.
|
||||||
|
|
||||||
|
This new functionality streamlines database management, ensuring that backups are always available and easy to manage, thus improving the reliability and resilience of the application.
|
10
Dockerfile
10
Dockerfile
|
@ -1,5 +1,5 @@
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
|
@ -39,16 +39,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||||
libreoffice \
|
libreoffice \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 && \
|
python3 \
|
||||||
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
py3-pip && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|
|
@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
|
||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
|
@ -31,7 +31,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022 \
|
UMASK=022 \
|
||||||
FAT_DOCKER=true \
|
FAT_DOCKER=true \
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=true
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
|
@ -45,7 +45,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
calibre@testing \
|
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
|
@ -62,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 && \
|
python3 \
|
||||||
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
py3-pip && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
|
135
README.md
135
README.md
|
@ -26,6 +26,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
||||||
- Parallel file processing and downloads
|
- Parallel file processing and downloads
|
||||||
- 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://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||||
|
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
||||||
|
|
||||||
## **PDF Features**
|
## **PDF Features**
|
||||||
|
|
||||||
|
@ -164,42 +165,46 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
|
||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling PDF currently supports 32!
|
Stirling PDF currently supports 38!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| ------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
|
| Arabic (العربية) (ar_AR) | ![44%](https://geps.dev/progress/44) |
|
||||||
|
| Basque (Euskara) (eu_ES) | ![60%](https://geps.dev/progress/60) |
|
||||||
|
| Bulgarian (Български) (bg_BG) | ![92%](https://geps.dev/progress/92) |
|
||||||
|
| Catalan (Català) (ca_CA) | ![47%](https://geps.dev/progress/47) |
|
||||||
|
| Croatian (Hrvatski) (hr_HR) | ![92%](https://geps.dev/progress/92) |
|
||||||
|
| Czech (Česky) (cs_CZ) | ![88%](https://geps.dev/progress/88) |
|
||||||
|
| Danish (Dansk) (da_DK) | ![9%](https://geps.dev/progress/9) |
|
||||||
|
| Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) |
|
||||||
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
|
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
|
||||||
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
|
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
|
||||||
| Arabic (العربية) (ar_AR) | ![46%](https://geps.dev/progress/46) |
|
| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
|
||||||
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
|
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
|
||||||
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) |
|
| Greek (Ελληνικά) (el_GR) | ![80%](https://geps.dev/progress/80) |
|
||||||
| Spanish (Español) (es_ES) | ![93%](https://geps.dev/progress/93) |
|
| Hindi (हिंदी) (hi_IN) | ![75%](https://geps.dev/progress/75) |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) | ![99%](https://geps.dev/progress/99) |
|
| Hungarian (Magyar) (hu_HU) | ![74%](https://geps.dev/progress/74) |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
|
| Indonesia (Bahasa Indonesia) (id_ID) | ![74%](https://geps.dev/progress/74) |
|
||||||
| Catalan (Català) (ca_CA) | ![49%](https://geps.dev/progress/49) |
|
| Irish (Gaeilge) (ga_IE) | ![96%](https://geps.dev/progress/96) |
|
||||||
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
|
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
|
||||||
| Swedish (Svenska) (sv_SE) | ![40%](https://geps.dev/progress/40) |
|
| Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) |
|
||||||
| Polish (Polski) (pl_PL) | ![92%](https://geps.dev/progress/92) |
|
| Korean (한국어) (ko_KR) | ![82%](https://geps.dev/progress/82) |
|
||||||
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) |
|
| Norwegian (Norsk) (no_NB) | ![96%](https://geps.dev/progress/96) |
|
||||||
| Korean (한국어) (ko_KR) | ![86%](https://geps.dev/progress/86) |
|
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) | ![61%](https://geps.dev/progress/61) |
|
| Portuguese (Português) (pt_PT) | ![76%](https://geps.dev/progress/76) |
|
||||||
| Portuguese (Português) (pt_PT) | ![80%](https://geps.dev/progress/80) |
|
| Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) |
|
||||||
| Russian (Русский) (ru_RU) | ![86%](https://geps.dev/progress/86) |
|
| Romanian (Română) (ro_RO) | ![38%](https://geps.dev/progress/38) |
|
||||||
| Basque (Euskara) (eu_ES) | ![63%](https://geps.dev/progress/63) |
|
| Russian (Русский) (ru_RU) | ![82%](https://geps.dev/progress/82) |
|
||||||
| Japanese (日本語) (ja_JP) | ![92%](https://geps.dev/progress/92) |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![76%](https://geps.dev/progress/76) |
|
||||||
| Dutch (Nederlands) (nl_NL) | ![98%](https://geps.dev/progress/98) |
|
| Simplified Chinese (简体中文) (zh_CN) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Greek (Ελληνικά) (el_GR) | ![84%](https://geps.dev/progress/84) |
|
| Slovakian (Slovensky) (sk_SK) | ![90%](https://geps.dev/progress/90) |
|
||||||
| Turkish (Türkçe) (tr_TR) | ![96%](https://geps.dev/progress/96) |
|
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
|
||||||
| Indonesia (Bahasa Indonesia) (id_ID) | ![78%](https://geps.dev/progress/78) |
|
| Swedish (Svenska) (sv_SE) | ![38%](https://geps.dev/progress/38) |
|
||||||
| Hindi (हिंदी) (hi_IN) | ![78%](https://geps.dev/progress/78) |
|
| Thai (ไทย) (th_TH) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Hungarian (Magyar) (hu_HU) | ![77%](https://geps.dev/progress/77) |
|
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
|
||||||
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
|
| Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![80%](https://geps.dev/progress/80) |
|
| Ukrainian (Українська) (uk_UA) | ![88%](https://geps.dev/progress/88) |
|
||||||
| Ukrainian (Українська) (uk_UA) | ![92%](https://geps.dev/progress/92) |
|
| Vietnamese (Tiếng Việt) (vi_VN) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Slovakian (Slovensky) (sk_SK) | ![93%](https://geps.dev/progress/93) |
|
|
||||||
| Czech (Česky) (cs_CZ) | ![92%](https://geps.dev/progress/92) |
|
|
||||||
| Croatian (Hrvatski) (hr_HR) | ![97%](https://geps.dev/progress/97) |
|
|
||||||
| Norwegian (Norsk) (no_NB) | ![97%](https://geps.dev/progress/97) |
|
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
|
@ -232,37 +237,39 @@ The Current list of settings is
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries
|
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
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
# initialLogin:
|
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)
|
||||||
# username: "admin" # Initial username for the first login
|
initialLogin:
|
||||||
# password: "stirling" # Initial password for the first login
|
username: '' # Initial username for the first login
|
||||||
# oauth2:
|
password: '' # Initial password for the first login
|
||||||
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
oauth2:
|
||||||
# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
# clientId: "" # Client ID from your provider
|
client:
|
||||||
# clientSecret: "" # Client Secret from your provider
|
keycloak:
|
||||||
# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
||||||
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
|
clientId: '' # Client ID for Keycloak OAuth2
|
||||||
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions
|
clientSecret: '' # Client Secret for Keycloak OAuth2
|
||||||
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
scopes: openid, profile, email # Scopes for Keycloak OAuth2
|
||||||
# client:
|
useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
|
||||||
# google:
|
google:
|
||||||
# clientId: "" # Client ID for Google OAuth2
|
clientId: '' # Client ID for Google OAuth2
|
||||||
# clientSecret: "" # Client Secret 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
|
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
|
useAsUsername: email # Field to use as the username for Google OAuth2
|
||||||
# github:
|
github:
|
||||||
# clientId: "" # Client ID for GitHub OAuth2
|
clientId: '' # Client ID for GitHub OAuth2
|
||||||
# clientSecret: "" # Client Secret for GitHub OAuth2
|
clientSecret: '' # Client Secret for GitHub OAuth2
|
||||||
# scopes: "read:user" # Scope for GitHub OAuth2
|
scopes: read:user # Scope for GitHub OAuth2
|
||||||
# useAsUsername: "login" # Field to use as the username for GitHub OAuth2
|
useAsUsername: login # Field to use as the username for GitHub OAuth2
|
||||||
# keycloak:
|
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
clientId: '' # Client ID from your provider
|
||||||
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2
|
clientSecret: '' # Client Secret from your provider
|
||||||
# clientSecret: "" # Client Secret for Keycloak OAuth2
|
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2
|
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
||||||
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2
|
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'
|
||||||
|
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||||
|
@ -273,9 +280,9 @@ system:
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appName: null # Application's visible name
|
appName: '' # Application's visible name
|
||||||
homeDescription: null # Short description or tagline shown on homepage.
|
homeDescription: '' # Short description or tagline shown on homepage.
|
||||||
appNameNavbar: null # Name displayed on the navigation bar
|
appNameNavbar: '' # Name displayed on the navigation bar
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
|
@ -309,7 +316,7 @@ For those wanting to use Stirling-PDFs backend API to link with their own custom
|
||||||
|
|
||||||
![stirling-login](images/login-light.png)
|
![stirling-login](images/login-light.png)
|
||||||
|
|
||||||
### Prerequisites:
|
### Prerequisites
|
||||||
|
|
||||||
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
- 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.
|
- Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
|
||||||
|
|
107
build.gradle
107
build.gradle
|
@ -1,25 +1,33 @@
|
||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.3.0"
|
id "org.springframework.boot" version "3.3.2"
|
||||||
id "io.spring.dependency-management" version "1.1.5"
|
id "io.spring.dependency-management" version "1.1.6"
|
||||||
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.5"
|
id "edu.sc.seis.launch4j" version "3.0.6"
|
||||||
id "com.diffplug.spotless" version "6.25.0"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.8"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
|
//id "nebula.lint" version "19.0.3"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.3.0"
|
springBootVersion = "3.3.2"
|
||||||
|
pdfboxVersion = "3.0.3"
|
||||||
|
logbackVersion = "1.5.7"
|
||||||
|
imageioVersion = "3.11.0"
|
||||||
|
lombokVersion = "1.18.34"
|
||||||
|
bouncycastleVersion = "1.78.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.26.1"
|
version = "0.28.3"
|
||||||
|
|
||||||
// 17 is lowest but we support and recommend 21
|
java {
|
||||||
sourceCompatibility = "17"
|
// 17 is lowest but we support and recommend 21
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -36,10 +44,14 @@ sourceSets {
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||||
exclude "stirling/software/SPDF/config/security/**"
|
exclude "stirling/software/SPDF/config/security/**"
|
||||||
exclude "stirling/software/SPDF/controller/api/UserController.java"
|
exclude "stirling/software/SPDF/controller/api/UserController.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
|
||||||
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||||
|
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
||||||
exclude "stirling/software/SPDF/model/Authority.java"
|
exclude "stirling/software/SPDF/model/Authority.java"
|
||||||
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
||||||
|
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
exclude "stirling/software/SPDF/repository/**"
|
exclude "stirling/software/SPDF/repository/**"
|
||||||
}
|
}
|
||||||
|
@ -89,37 +101,42 @@ spotless {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//gradleLint {
|
||||||
|
// rules=['unused-dependency']
|
||||||
|
// }
|
||||||
tasks.wrapper {
|
tasks.wrapper {
|
||||||
gradleVersion = "8.7"
|
gradleVersion = "8.7"
|
||||||
}
|
}
|
||||||
|
//tasks.withType(JavaCompile) {
|
||||||
|
// options.compilerArgs << "-Xlint:deprecation"
|
||||||
|
//}
|
||||||
|
configurations.all {
|
||||||
|
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||||
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//security updates
|
//security updates
|
||||||
implementation "ch.qos.logback:logback-classic:1.5.6"
|
|
||||||
implementation "ch.qos.logback:logback-core:1.5.6"
|
|
||||||
implementation "org.springframework:spring-webmvc:6.1.9"
|
implementation "org.springframework:spring-webmvc:6.1.9"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.1.3")
|
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||||
|
|
||||||
// implementation "org.yaml:snakeyaml:2.2"
|
// implementation "org.yaml:snakeyaml:2.2"
|
||||||
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
||||||
|
|
||||||
// Exclude Tomcat and include Jetty
|
// Exclude Tomcat and include Jetty
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") {
|
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
|
||||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
|
||||||
}
|
|
||||||
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
||||||
|
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||||
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
runtimeOnly "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
implementation "com.h2database:h2:2.1.214"
|
runtimeOnly "com.h2database:h2:2.1.214"
|
||||||
|
// implementation "com.h2database:h2:2.2.224"
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
@ -128,26 +145,25 @@ dependencies {
|
||||||
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
||||||
|
|
||||||
// TwelveMonkeys
|
// TwelveMonkeys
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-batik:3.10.1"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-bmp:3.10.1"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-hdr:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-icns:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-iff:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-jpeg:3.11.0"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pcx:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pict:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pnm:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-psd:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-sgi:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-tga:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-tiff:3.10.1"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-webp:3.10.1"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-xwd:3.10.1"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
||||||
|
|
||||||
implementation "commons-io:commons-io:2.16.1"
|
implementation "commons-io:commons-io:2.16.1"
|
||||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||||
|
|
||||||
//general PDF
|
//general PDF
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
|
@ -155,33 +171,36 @@ dependencies {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:pdfbox:3.0.2") {
|
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:xmpbox:3.0.2") {
|
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
|
|
||||||
|
|
||||||
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
||||||
|
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:1.78.1"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:1.78.1"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
implementation "io.micrometer:micrometer-core:1.13.0"
|
implementation "io.micrometer:micrometer-core:1.13.3"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.22.0"
|
implementation "org.commonmark:commonmark:0.22.0"
|
||||||
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
||||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||||
implementation "com.bucket4j:bucket4j_jdk17-core:8.12.1"
|
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
|
||||||
|
|
||||||
implementation "com.fathzer:javaluator:3.0.4"
|
implementation "com.fathzer:javaluator:3.0.4"
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||||
compileOnly "org.projectlombok:lombok:1.18.32"
|
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
annotationProcessor "org.projectlombok:lombok:1.18.32"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
testImplementation 'org.mockito:mockito-inline:3.12.4'
|
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.26.1
|
appVersion: 0.28.3
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
|
|
@ -62,8 +62,10 @@ spec:
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
||||||
{{- if .Values.envs }}
|
|
||||||
env:
|
env:
|
||||||
|
- name: SYSTEM_ROOTURIPATH
|
||||||
|
value: {{ .Values.rootPath}}
|
||||||
|
{{- if .Values.envs }}
|
||||||
{{ toYaml .Values.envs | indent 8 }}
|
{{ toYaml .Values.envs | indent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.extraArgs }}
|
{{- if .Values.extraArgs }}
|
||||||
|
@ -75,13 +77,13 @@ spec:
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: {{ .Values.rootPath}}
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.liveness | indent 10 }}
|
{{ toYaml .Values.probes.liveness | indent 10 }}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: {{ .Values.rootPath}}
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.readiness | indent 10 }}
|
{{ toYaml .Values.probes.readiness | indent 10 }}
|
||||||
|
|
|
@ -15,6 +15,9 @@ secret:
|
||||||
commonLabels: {}
|
commonLabels: {}
|
||||||
# team_name: dev
|
# team_name: dev
|
||||||
|
|
||||||
|
# rootpath for the application
|
||||||
|
rootPath: /
|
||||||
|
|
||||||
envs: []
|
envs: []
|
||||||
# - name: UI_APP_NAME
|
# - name: UI_APP_NAME
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
|
@ -24,8 +27,6 @@ envs: []
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
# - name: ALLOW_GOOGLE_VISIBILITY
|
# - name: ALLOW_GOOGLE_VISIBILITY
|
||||||
# value: "true"
|
# value: "true"
|
||||||
# - name: APP_ROOT_PATH
|
|
||||||
# value: "/"
|
|
||||||
# - name: APP_LOCALE
|
# - name: APP_LOCALE
|
||||||
# value: "en_GB"
|
# value: "en_GB"
|
||||||
|
|
||||||
|
|
106
cucumber/exampleFiles/ghost1.pdf
Normal file
106
cucumber/exampleFiles/ghost1.pdf
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@Gb79+X'F"5[`EfJOD4:mD<%*=m+N>oDG,>NK`<U'B^0WYY,dWl^i_UcRk`<"L=<NPC$BtQ<5l$3<Y!?BuoCSYQ6GSt25lpqr0IrP?S[b)9%M"e'HHFqcRO'9eRaR0'DYi*Y.:nEMFAoTM;rPL%EF]`CfoELVl_Q,"LS:%iI;Nc[&bG.*65O]ecfK1'*<>5P_s[usI/ph*0pV~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@Gb79+X'F"5Y`EfJOV2A9=!fB]F'tK1LS`,]G+MiTenb&V2-^hqa(5IE#Nr59/!"Qm*5_(BdF!0&h!Yhk/A+\iS'%6tuO$O)9LaZS+flr([1p2&#RS1p/gT[B;rDj-=&=iqUlj(P^/5U@eCFqn4:<lU`l`.HXqG-',hJH.DI.(6L\luSAW`Q'oje[qgVLVIXg%PXe+,<$7('~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GbmK%f(e+0_`ODoa2.):e/i+N3r(.o*Qf\gSNb(bt4FIubi@GIOE=p8Ir3;CbQ@KuG^cdJhODZKQ*upt+*rdZ%!mFmN$*.P)K;`s#]G=8AO3s3DGB.RCOn?[F]bEIg,a>25?B%dh\Z/C6opFE'el@I,P\u\V\]:*JYrrsNJ&d,11VL;$h!43eGu&1X6$+5-h\Vr6!+>4Je,~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001527 00000 n
|
||||||
|
0000001827 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<0d5cf047e754e05f8d574f067785875c><0d5cf047e754e05f8d574f067785875c>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2127
|
||||||
|
%%EOF
|
106
cucumber/exampleFiles/ghost2.pdf
Normal file
106
cucumber/exampleFiles/ghost2.pdf
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G:CDb.*/<p2MVk["e@)7*Z0@"b%+@f/9pA%_U<oOkVp?PnGRb81iPg?0i?(]%^_CSf##%;<!7Ne/-%RR^p@t7hKYZ9eJVHV]fjjHIB:6DrW+2\p16@*`r^CpQZZH'2Pjqd<.&hM2UO%$Wi$te%4QmS;<E"QS\!deQG_XtuEK>b(UbS>%`/0S`k\\5'TNY0mmgH?`8]i_0~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]afWJ'Lm;=if<;s>V*7BTJ]oQ@P!(q5S+WG1%>L@?8Ue;c>[fY&&IOd5@t@TY@+q.5T<Z'81"J("KhsBa+&u4"n'#6)AjfImh)%$0tVC:aGk",=aJJH#/4]i.WJr9c"cibYm:M-44<%FFlG0Cl\Z'nmo7C"TR+7dk3T#iD(9Pq'\;rQku%o>A_`50SO&7M04=8M'O<Am~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GYmu@>'Ld5[if35r/JNaJ.A.7fP9RpSN*8k^-sEER0,enq1Rsuo@R/uCO-^&Y`F'9d^a?9)?ns+F&dXm[HMgPn6Ep+%TRk5Nh+!(+[H#H:U^.^(YL,PKS'%j/:3O\hJVEK-UUekJTd[A$N^((K^#0Du`i@,/^f5KiUISGr")3/+f9NF8NO1+iUgm^b"X\cE^+[:s!0]Gu6i~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001524 00000 n
|
||||||
|
0000001822 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<407fc55425168745e56176202aad30c9><407fc55425168745e56176202aad30c9>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2122
|
||||||
|
%%EOF
|
106
cucumber/exampleFiles/ghost3.pdf
Normal file
106
cucumber/exampleFiles/ghost3.pdf
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>:>hu1e>07BJg5<'#:.C1n)e#(QJ6R1Rsuo_gpn.+0-H5$/#"iYR[B.9\'>7!aDAC*rf/t&6O#aH<?-7IT'\?X(&TcABG=ON*Nq`4k=o&p@3,0*31r<)TAP2Pk94p0\"R-_sY1$AYo[8B\?4R>feLAB\mpjZhp"`@J3;"Fm97#9+W,"eb95\+#p\^HN~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EX'Eriuig+>QHNeD'#n%Sq#n%BW`C'uDUOYK)HdS4E9JMsp+HUmDj&H-t*4?UamXX0peVspk"i_@ba+&u"J>UYDKV_^G,7V==aTZZ<YO7:sNSQ[6"Ja-29NtYjd#=`J@D'h+[QW=:EEb?A<k!f+\`g^?,Vgp7_)91[lR\f.Tkf7VIPLVYM&deF!aYt9Ip^"N",3F'*W~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>J`g!jPCLm;?AgU"fdk"PQZD\d?lRI_oWc[$tp^]O\:3fK8kWeX2&Jcg0+RoJ]j;2j*upu!b4.o&f)b$I@7CfIYjP^#\VjhC=QhQ]^lV-@<0Tam!0.+Dn@("AK%N,Uc7hb+6VoQ$q2q[7]BB92RoY/.j2N028i1jNf'@<1+Fqf$1&"8omHk`#DHP>OT~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001526 00000 n
|
||||||
|
0000001826 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<80da26147a484f2b7573da8151a93d2e><80da26147a484f2b7573da8151a93d2e>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2126
|
||||||
|
%%EOF
|
1255
cucumber/exampleFiles/images.pdf
Normal file
1255
cucumber/exampleFiles/images.pdf
Normal file
File diff suppressed because it is too large
Load diff
106
cucumber/exampleFiles/pdfa1.pdf
Normal file
106
cucumber/exampleFiles/pdfa1.pdf
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 206
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G\IO3f&4Lr[@S4&T2aReWZ3N'9",Ncra>5AuK^J(o@r?=EP>b]h[L@XZ8q7#[c:#H2:^/=b,p3^,&f-Q.'H%!U?%N\iVa1pLMlh/41\A8@dF5@0al:-1?L;D%LpL3g\9`.3c6N/Mp=sE/nO%^@%Cc3`]e`qqS@[pkUWemMZC<P\fkqa55u)*hIUoU437-gb!e_*&B/,&~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G\IO3V'LdA_ig"8P1PS=kA5Q_GQ\P]*S3\>Q`jHYt?8UdkV`6]UV*On)+1VMV+A@.iF:*6sWfM9f"s.NmVuMto!p7-+,Rb<.h,pdi-&OQ5KO\RRFj.j"A)ScTQ7$hudF^TnZ'XuQA5"O]rYkt><-DJmj'"Ri>n!4`^m409XX`e)AR'*rGsn6m79.18+^ba=qRuss"-A3k+9~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:.1fBHK`Xl'[i1&AjX(\k8hbgo(QJ6R1Rsuo6_I1A5Gg$JL;D#$J2CX;+Cf*cUHk2%H1XmpWe+qZ5moJ#B]>b%%[d,mfSSkS4A:Q4NlOFfrL7eA,s45"eUSakM;927AA,1"-LZ)&nZ/ah=8_X7:?ZMj@J@;r7d`t]Z0\d39M%:$k8[S5D"2oSap4s80l?~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001523 00000 n
|
||||||
|
0000001823 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<88edee24ee67bd7d6b7cf53cfa2222b0><88edee24ee67bd7d6b7cf53cfa2222b0>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2124
|
||||||
|
%%EOF
|
106
cucumber/exampleFiles/pdfa2.pdf
Normal file
106
cucumber/exampleFiles/pdfa2.pdf
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GYmu@>'Ld5[if35rI0]sG)F[U^"c>T)"\\os-r:1V0,enq1Rsuo,*67.@k7U.LRF-P.e"CM2V!>iYi<g`nXh!K?n@$t^rY1$+^0'>=B8H6e;F1WmG#,(eS00(Qe9&:O@nI879DTsT,njXAB?`8:>,Hn3*RV!qh4;&@6%]<9Y*>QZ].Z5o;RAZXg7d[#+bphHs_Ep!QR2TZ2~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>=,iY1bE)XN?M;1'J/>i&HY;gks]*rj:!DKpb8@`prC#N+9E#o#-<G*!#p7e6j-1sX2k5S,6XmM"taYkfK^k">%usEeEk=sR<UT"dm`rXD;!S`_jS9LU+(R%e'V%WSMfHP.pXZEQqTQq=&D[I[PS(41(NIAZ1R/U?:Z=hSXu!NDF)bpG2F+/I/q/u1-Y~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G_$YcZ'LhbF`EQB$nqi=8S<;#HbK3&f>rnodRPo`Vf4P[3cJidY(I=[K5NWCT'<lHgci?oCRVNST&[k#q4oSC0FWgAt1pD4d_(hIRjn_Nt+cFgJlfm[1U8@/M4r^Pk<@F!@e?%/!-Vq;]nfdLi9]P2M)ck9?)%oNXa_\N<-d"(pjlH%-G`T@Sj&P(j6.@#Xh\Vr6!1iI2/H~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001526 00000 n
|
||||||
|
0000001827 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<4fcc82a085fe71e34a32d1b23c8b939f><4fcc82a085fe71e34a32d1b23c8b939f>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2127
|
||||||
|
%%EOF
|
|
@ -14,3 +14,8 @@ def after_scenario(context, scenario):
|
||||||
os.remove('response_file')
|
os.remove('response_file')
|
||||||
if hasattr(context, 'file_name') and os.path.exists(context.file_name):
|
if hasattr(context, 'file_name') and os.path.exists(context.file_name):
|
||||||
os.remove(context.file_name)
|
os.remove(context.file_name)
|
||||||
|
|
||||||
|
# Remove any temporary files
|
||||||
|
for temp_file in os.listdir('.'):
|
||||||
|
if temp_file.startswith('genericNonCustomisableName') or temp_file.startswith('temp_image_'):
|
||||||
|
os.remove(temp_file)
|
|
@ -1,4 +1,4 @@
|
||||||
@example
|
@example @general
|
||||||
Feature: API Validation
|
Feature: API Validation
|
||||||
|
|
||||||
@positive @password
|
@positive @password
|
||||||
|
@ -92,10 +92,10 @@ Feature: API Validation
|
||||||
| threshold | 90 |
|
| threshold | 90 |
|
||||||
| whitePercent | 99.9 |
|
| whitePercent | 99.9 |
|
||||||
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
||||||
Then the response content type should be "application/pdf"
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response file should have extension ".zip"
|
||||||
|
And the response ZIP should contain 1 files
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
And the response PDF should contain 0 pages
|
|
||||||
And the response status code should be 200
|
|
||||||
|
|
||||||
@positive @flatten
|
@positive @flatten
|
||||||
Scenario: Flatten PDF
|
Scenario: Flatten PDF
|
||||||
|
|
|
@ -32,7 +32,7 @@ Feature: API Validation
|
||||||
@ocr @positive
|
@ocr @positive
|
||||||
Scenario: Extract Image Scans
|
Scenario: Extract Image Scans
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I generate a PDF file as "fileInput"
|
||||||
And the pdf contains 3 images on 2 pages
|
And the pdf contains 3 images of size 300x300 on 2 pages
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| angleThreshold | 5 |
|
| angleThreshold | 5 |
|
||||||
|
@ -125,8 +125,7 @@ Feature: API Validation
|
||||||
|
|
||||||
@ocr
|
@ocr
|
||||||
Scenario: PDFA
|
Scenario: PDFA
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| outputFormat | pdfa |
|
| outputFormat | pdfa |
|
||||||
|
@ -137,8 +136,7 @@ Feature: API Validation
|
||||||
|
|
||||||
@ocr
|
@ocr
|
||||||
Scenario: PDFA1
|
Scenario: PDFA1
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| outputFormat | pdfa-1 |
|
| outputFormat | pdfa-1 |
|
||||||
|
@ -149,8 +147,7 @@ Feature: API Validation
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 4 |
|
| optimizeLevel | 4 |
|
||||||
|
@ -161,8 +158,7 @@ Feature: API Validation
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 1 |
|
| optimizeLevel | 1 |
|
||||||
|
@ -175,8 +171,7 @@ Feature: API Validation
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 1 |
|
| optimizeLevel | 1 |
|
||||||
|
|
|
@ -94,3 +94,23 @@ Feature: API Validation
|
||||||
| 1 | 10 | 2 | 10 |
|
| 1 | 10 | 2 | 10 |
|
||||||
|
|
||||||
|
|
||||||
|
@extract-images
|
||||||
|
Scenario Outline: Extract Image Scans
|
||||||
|
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| format | <format> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/extract-images"
|
||||||
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response file should have extension ".zip"
|
||||||
|
And the response ZIP should contain 20 files
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| format |
|
||||||
|
| png |
|
||||||
|
| gif |
|
||||||
|
| jpeg |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,14 @@ import io
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from reportlab.lib.pagesizes import letter
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.lib.utils import ImageReader
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import requests
|
import requests
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
|
import re
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# GIVEN #
|
# GIVEN #
|
||||||
|
@ -43,8 +46,6 @@ def step_use_example_file(context, filePath, fileInput):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@given('the pdf contains {page_count:d} pages')
|
@given('the pdf contains {page_count:d} pages')
|
||||||
def step_pdf_contains_pages(context, page_count):
|
def step_pdf_contains_pages(context, page_count):
|
||||||
writer = PdfWriter()
|
writer = PdfWriter()
|
||||||
|
@ -66,8 +67,6 @@ def step_pdf_contains_blank_pages(context, page_count):
|
||||||
context.files[context.param_name].close()
|
context.files[context.param_name].close()
|
||||||
context.files[context.param_name] = open(context.file_name, 'rb')
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_black_box_image(file_name, size):
|
def create_black_box_image(file_name, size):
|
||||||
can = canvas.Canvas(file_name, pagesize=size)
|
can = canvas.Canvas(file_name, pagesize=size)
|
||||||
width, height = size
|
width, height = size
|
||||||
|
@ -76,9 +75,25 @@ def create_black_box_image(file_name, size):
|
||||||
can.showPage()
|
can.showPage()
|
||||||
can.save()
|
can.save()
|
||||||
|
|
||||||
def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
@given(u'the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages')
|
||||||
page_width, page_height = letter
|
def step_impl(context, image_count, width, height, page_count):
|
||||||
box_size = 72 # 1 inch by 1 inch black box
|
context.param_name = "fileInput"
|
||||||
|
context.file_name = "genericNonCustomisableName.pdf"
|
||||||
|
create_pdf_with_images_and_boxes(context.file_name, image_count, page_count, width, height)
|
||||||
|
if not hasattr(context, 'files'):
|
||||||
|
context.files = {}
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
def add_black_boxes_to_image(image):
|
||||||
|
if isinstance(image, str):
|
||||||
|
image = Image.open(image)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
draw.rectangle([(0, 0), image.size], fill=(0, 0, 0)) # Fill image with black
|
||||||
|
return image
|
||||||
|
|
||||||
|
def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_width, image_height):
|
||||||
|
page_width, page_height = max(letter[0], image_width), max(letter[1], image_height)
|
||||||
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
|
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
|
||||||
|
|
||||||
writer = PdfWriter()
|
writer = PdfWriter()
|
||||||
|
@ -86,15 +101,31 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
||||||
|
|
||||||
for page in range(page_count):
|
for page in range(page_count):
|
||||||
packet = io.BytesIO()
|
packet = io.BytesIO()
|
||||||
can = canvas.Canvas(packet, pagesize=letter)
|
can = canvas.Canvas(packet, pagesize=(page_width, page_height))
|
||||||
|
|
||||||
for i in range(boxes_per_page):
|
for i in range(boxes_per_page):
|
||||||
if box_counter >= image_count:
|
if box_counter >= image_count:
|
||||||
break
|
break
|
||||||
x = (i % (page_width // box_size)) * box_size
|
|
||||||
y = page_height - ((i // (page_width // box_size) + 1) * box_size)
|
# Simulating a dynamic image creation (replace this with your actual image creation logic)
|
||||||
can.setFillColorRGB(0, 0, 0)
|
# For demonstration, we'll create a simple black image
|
||||||
can.rect(x, y, box_size, box_size, fill=1)
|
dummy_image = Image.new('RGB', (image_width, image_height), color='white') # Create a white image
|
||||||
|
dummy_image = add_black_boxes_to_image(dummy_image) # Add black boxes
|
||||||
|
|
||||||
|
# Convert the PIL Image to bytes to pass to drawImage
|
||||||
|
image_bytes = io.BytesIO()
|
||||||
|
dummy_image.save(image_bytes, format='PNG')
|
||||||
|
image_bytes.seek(0)
|
||||||
|
|
||||||
|
# Check if the image fits in the current page dimensions
|
||||||
|
x = (i % (page_width // image_width)) * image_width
|
||||||
|
y = page_height - (((i % (page_height // image_height)) + 1) * image_height)
|
||||||
|
|
||||||
|
if x + image_width > page_width or y < 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add the image to the PDF
|
||||||
|
can.drawImage(ImageReader(image_bytes), x, y, width=image_width, height=image_height)
|
||||||
box_counter += 1
|
box_counter += 1
|
||||||
|
|
||||||
can.showPage()
|
can.showPage()
|
||||||
|
@ -103,9 +134,16 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
||||||
new_pdf = PdfReader(packet)
|
new_pdf = PdfReader(packet)
|
||||||
writer.add_page(new_pdf.pages[0])
|
writer.add_page(new_pdf.pages[0])
|
||||||
|
|
||||||
|
# Write the PDF to file
|
||||||
with open(file_name, 'wb') as f:
|
with open(file_name, 'wb') as f:
|
||||||
writer.write(f)
|
writer.write(f)
|
||||||
|
|
||||||
|
# Clean up temporary image files
|
||||||
|
for i in range(image_count):
|
||||||
|
temp_image_path = f"temp_image_{i}.png"
|
||||||
|
if os.path.exists(temp_image_path):
|
||||||
|
os.remove(temp_image_path)
|
||||||
|
|
||||||
@given('the pdf contains {image_count:d} images on {page_count:d} pages')
|
@given('the pdf contains {image_count:d} images on {page_count:d} pages')
|
||||||
def step_pdf_contains_images(context, image_count, page_count):
|
def step_pdf_contains_images(context, image_count, page_count):
|
||||||
if not hasattr(context, 'param_name'):
|
if not hasattr(context, 'param_name'):
|
||||||
|
@ -118,7 +156,6 @@ def step_pdf_contains_images(context, image_count, page_count):
|
||||||
context.files[context.param_name].close()
|
context.files[context.param_name].close()
|
||||||
context.files[context.param_name] = open(context.file_name, 'rb')
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
@given('the pdf contains {page_count:d} pages with random text')
|
@given('the pdf contains {page_count:d} pages with random text')
|
||||||
def step_pdf_contains_pages_with_random_text(context, page_count):
|
def step_pdf_contains_pages_with_random_text(context, page_count):
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
@ -186,6 +223,21 @@ def save_generated_pdf(context, filename):
|
||||||
# WHEN #
|
# WHEN #
|
||||||
########
|
########
|
||||||
|
|
||||||
|
@when('I send a GET request to "{endpoint}"')
|
||||||
|
def step_send_get_request(context, endpoint):
|
||||||
|
base_url = "http://localhost:8080"
|
||||||
|
full_url = f"{base_url}{endpoint}"
|
||||||
|
response = requests.get(full_url)
|
||||||
|
context.response = response
|
||||||
|
|
||||||
|
@when('I send a GET request to "{endpoint}" with parameters')
|
||||||
|
def step_send_get_request_with_params(context, endpoint):
|
||||||
|
base_url = "http://localhost:8080"
|
||||||
|
params = {row['parameter']: row['value'] for row in context.table}
|
||||||
|
full_url = f"{base_url}{endpoint}"
|
||||||
|
response = requests.get(full_url, params=params)
|
||||||
|
context.response = response
|
||||||
|
|
||||||
@when('I send the API request to the endpoint "{endpoint}"')
|
@when('I send the API request to the endpoint "{endpoint}"')
|
||||||
def step_send_api_request(context, endpoint):
|
def step_send_api_request(context, endpoint):
|
||||||
url = f"http://localhost:8080{endpoint}"
|
url = f"http://localhost:8080{endpoint}"
|
||||||
|
@ -278,7 +330,6 @@ def step_save_response_file(context, filename):
|
||||||
f.write(context.response.content)
|
f.write(context.response.content)
|
||||||
print(f"Saved response content to {filename}")
|
print(f"Saved response content to {filename}")
|
||||||
|
|
||||||
|
|
||||||
@then('the response PDF should contain {page_count:d} pages')
|
@then('the response PDF should contain {page_count:d} pages')
|
||||||
def step_check_response_pdf_page_count(context, page_count):
|
def step_check_response_pdf_page_count(context, page_count):
|
||||||
response_file = io.BytesIO(context.response.content)
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
@ -305,3 +356,26 @@ def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc):
|
||||||
reader = PdfReader(pdf_file)
|
reader = PdfReader(pdf_file)
|
||||||
actual_pages_per_doc = len(reader.pages)
|
actual_pages_per_doc = len(reader.pages)
|
||||||
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
|
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
|
||||||
|
|
||||||
|
@then('the JSON value of "{key}" should be "{expected_value}"')
|
||||||
|
def step_check_json_value(context, key, expected_value):
|
||||||
|
actual_value = context.response.json().get(key)
|
||||||
|
assert actual_value == expected_value, \
|
||||||
|
f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'"
|
||||||
|
|
||||||
|
@then('JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"')
|
||||||
|
def step_check_json_list_entry(context, identifier_key, identifier_self, target_key, target_value):
|
||||||
|
json_response = context.response.json()
|
||||||
|
for entry in json_response:
|
||||||
|
if entry.get(identifier_key) == identifier_value:
|
||||||
|
assert entry.get(target_key) == target_value, \
|
||||||
|
f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"No entry with {identifier_key} as {identifier_value} found")
|
||||||
|
|
||||||
|
@then('the response should match the regex "{pattern}"')
|
||||||
|
def step_response_matches_regex(context, pattern):
|
||||||
|
response_text = context.response.text
|
||||||
|
assert re.match(pattern, response_text), \
|
||||||
|
f"Response '{response_text}' does not match the expected pattern '{pattern}'"
|
||||||
|
|
|
@ -22,7 +22,6 @@ services:
|
||||||
DOCKER_ENABLE_SECURITY: "false"
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||||
|
|
|
@ -10,7 +10,11 @@ ignore = [
|
||||||
|
|
||||||
[ca_CA]
|
[ca_CA]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'PDFToText.tags',
|
||||||
|
'adminUserSettings.admin',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'survey.button',
|
||||||
|
'watermark.type.1',
|
||||||
]
|
]
|
||||||
|
|
||||||
[cs_CZ]
|
[cs_CZ]
|
||||||
|
@ -21,6 +25,11 @@ ignore = [
|
||||||
'text',
|
'text',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[da_DK]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[de_DE]
|
[de_DE]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
|
@ -48,6 +57,7 @@ ignore = [
|
||||||
ignore = [
|
ignore = [
|
||||||
'adminUserSettings.roles',
|
'adminUserSettings.roles',
|
||||||
'color',
|
'color',
|
||||||
|
'error',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'no',
|
'no',
|
||||||
'showJS.tags',
|
'showJS.tags',
|
||||||
|
@ -60,8 +70,31 @@ ignore = [
|
||||||
|
|
||||||
[fr_FR]
|
[fr_FR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'AddStampRequest.alphabet',
|
||||||
|
'AddStampRequest.position',
|
||||||
|
'AddStampRequest.rotation',
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
|
'addPageNumbers.selectText.3',
|
||||||
|
'adminUserSettings.actions',
|
||||||
|
'alphabet',
|
||||||
|
'compare.document.1',
|
||||||
|
'compare.document.2',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'licenses.license',
|
||||||
|
'licenses.module',
|
||||||
|
'licenses.nav',
|
||||||
|
'licenses.version',
|
||||||
|
'pdfOrganiser.mode',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
'sponsor',
|
'sponsor',
|
||||||
|
'watermark.type.2',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ga_IE]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
[hi_IN]
|
[hi_IN]
|
||||||
|
@ -71,6 +104,7 @@ ignore = [
|
||||||
|
|
||||||
[hr_HR]
|
[hr_HR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
'font',
|
'font',
|
||||||
'home.pipeline.title',
|
'home.pipeline.title',
|
||||||
'info',
|
'info',
|
||||||
|
@ -115,6 +149,7 @@ ignore = [
|
||||||
[nl_NL]
|
[nl_NL]
|
||||||
ignore = [
|
ignore = [
|
||||||
'HTMLToPDF.print',
|
'HTMLToPDF.print',
|
||||||
|
'adjustContrast.contrast',
|
||||||
'compare.document.1',
|
'compare.document.1',
|
||||||
'compare.document.2',
|
'compare.document.2',
|
||||||
'error',
|
'error',
|
||||||
|
@ -130,17 +165,25 @@ ignore = [
|
||||||
|
|
||||||
[no_NB]
|
[no_NB]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
|
'adminUserSettings.admin',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'oops',
|
||||||
|
'sponsor',
|
||||||
]
|
]
|
||||||
|
|
||||||
[pl_PL]
|
[pl_PL]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
[pt_BR]
|
[pt_BR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'changeMetadata.trapped',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
]
|
]
|
||||||
|
|
||||||
[pt_PT]
|
[pt_PT]
|
||||||
|
@ -160,12 +203,20 @@ ignore = [
|
||||||
|
|
||||||
[sk_SK]
|
[sk_SK]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'adminUserSettings.admin',
|
||||||
|
'home.multiTool.title',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'navbar.sections.security',
|
||||||
|
'text',
|
||||||
|
'watermark.type.1',
|
||||||
]
|
]
|
||||||
|
|
||||||
[sr_LATN_RS]
|
[sr_LATN_RS]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'licenses.version',
|
||||||
|
'poweredBy',
|
||||||
]
|
]
|
||||||
|
|
||||||
[sv_SE]
|
[sv_SE]
|
||||||
|
@ -173,6 +224,14 @@ ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[th_TH]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
[tr_TR]
|
[tr_TR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
@ -183,6 +242,14 @@ ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[vi_VN]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
[zh_CN]
|
[zh_CN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
|
|
@ -12,7 +12,8 @@ fi
|
||||||
umask "$UMASK" || true
|
umask "$UMASK" || true
|
||||||
|
|
||||||
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
|
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
|
||||||
apk add --no-cache calibre@testing
|
echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
|
||||||
|
#apk add --no-cache calibre@testing
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$FAT_DOCKER" != "true" ]]; then
|
if [[ "$FAT_DOCKER" != "true" ]]; then
|
||||||
|
|
174
scripts/png_to_webp.py
Normal file
174
scripts/png_to_webp.py
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
"""
|
||||||
|
Author: Ludy87
|
||||||
|
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
To convert a PDF file to WebP images with each page as a separate WebP file:
|
||||||
|
python script.py input.pdf output_directory
|
||||||
|
|
||||||
|
To convert a PDF file to a single WebP image:
|
||||||
|
python script.py input.pdf output_directory --single
|
||||||
|
|
||||||
|
To adjust the DPI resolution for rendering PDF pages:
|
||||||
|
python script.py input.pdf output_directory --dpi 150
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
|
||||||
|
"""
|
||||||
|
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_image_path : str
|
||||||
|
Path to the input image file.
|
||||||
|
output_image_path : str
|
||||||
|
Path where the output WebP image will be saved.
|
||||||
|
max_size : tuple of int, optional
|
||||||
|
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Open the image
|
||||||
|
image = Image.open(input_image_path)
|
||||||
|
width, height = image.size
|
||||||
|
max_width, max_height = max_size
|
||||||
|
|
||||||
|
# Check if the image dimensions exceed the maximum allowed dimensions
|
||||||
|
if width > max_width or height > max_height:
|
||||||
|
# Calculate the scaling ratio
|
||||||
|
ratio = min(max_width / width, max_height / height)
|
||||||
|
new_width = int(width * ratio)
|
||||||
|
new_height = int(height * ratio)
|
||||||
|
|
||||||
|
# Resize the image
|
||||||
|
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
resized_image.save(output_image_path, format="WEBP", quality=100)
|
||||||
|
print(
|
||||||
|
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# If dimensions are within the allowed limits, save the image directly
|
||||||
|
image.save(output_image_path, format="WEBP", quality=100)
|
||||||
|
print(f"The image was successfully saved as WebP: {output_image_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_image_to_webp(input_image, output_file):
|
||||||
|
"""
|
||||||
|
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_image : str
|
||||||
|
Path to the input image file.
|
||||||
|
output_file : str
|
||||||
|
Path where the output WebP image will be saved.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# Resize the image if it exceeds the maximum dimensions
|
||||||
|
resize_image(input_image, output_file, max_size=(16383, 16383))
|
||||||
|
|
||||||
|
|
||||||
|
def pdf_to_webp(pdf_path, output_dir, dpi=300):
|
||||||
|
"""
|
||||||
|
Convert each page of a PDF file to WebP images.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pdf_path : str
|
||||||
|
Path to the input PDF file.
|
||||||
|
output_dir : str
|
||||||
|
Directory where the WebP images will be saved.
|
||||||
|
dpi : int, optional
|
||||||
|
DPI resolution for rendering PDF pages. Default is 300.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# Convert the PDF to a list of images
|
||||||
|
images = convert_from_path(pdf_path, dpi=dpi)
|
||||||
|
|
||||||
|
for page_number, image in enumerate(images):
|
||||||
|
# Define temporary PNG path
|
||||||
|
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
|
||||||
|
image.save(temp_png_path, format="PNG")
|
||||||
|
|
||||||
|
# Define the output path for WebP
|
||||||
|
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
|
||||||
|
|
||||||
|
# Convert PNG to WebP
|
||||||
|
convert_image_to_webp(temp_png_path, output_path)
|
||||||
|
|
||||||
|
# Delete the temporary PNG file
|
||||||
|
os.remove(temp_png_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
|
||||||
|
"""
|
||||||
|
Main function to handle conversion from PDF to WebP images.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pdf_image_path : str
|
||||||
|
Path to the input PDF file or image.
|
||||||
|
output_dir : str
|
||||||
|
Directory where the WebP images will be saved.
|
||||||
|
dpi : int, optional
|
||||||
|
DPI resolution for rendering PDF pages. Default is 300.
|
||||||
|
single_images_flag : bool, optional
|
||||||
|
If True, combine all pages into a single WebP image. Default is False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if single_images_flag:
|
||||||
|
# Combine all pages into a single WebP image
|
||||||
|
output_path = os.path.join(output_dir, "combined_image.webp")
|
||||||
|
convert_image_to_webp(pdf_image_path, output_path)
|
||||||
|
else:
|
||||||
|
# Convert each PDF page to a separate WebP image
|
||||||
|
pdf_to_webp(pdf_image_path, output_dir, dpi)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
|
||||||
|
parser.add_argument("pdf_path", help="The path to the input PDF file.")
|
||||||
|
parser.add_argument(
|
||||||
|
"output_dir", help="The directory where the WebP images should be saved."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dpi",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="The DPI resolution for rendering the PDF pages (default: 300).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--single",
|
||||||
|
action="store_true",
|
||||||
|
help="Combine all pages into a single WebP image.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(args.output_dir, exist_ok=True)
|
||||||
|
main(
|
||||||
|
args.pdf_path,
|
||||||
|
args.output_dir,
|
||||||
|
dpi=args.dpi,
|
||||||
|
single_images_flag=args.single,
|
||||||
|
)
|
|
@ -45,7 +45,6 @@ public class SPdfApplication {
|
||||||
// Check if the BROWSER_OPEN environment variable is set to true
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||||
|
|
||||||
if (browserOpen) {
|
if (browserOpen) {
|
||||||
try {
|
try {
|
||||||
String url = "http://localhost:" + getNonStaticPort();
|
String url = "http://localhost:" + getNonStaticPort();
|
||||||
|
@ -66,6 +65,7 @@ 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);
|
||||||
|
app.setAdditionalProfiles("default");
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
|
@ -79,13 +79,14 @@ public class SPdfApplication {
|
||||||
|
|
||||||
// custom javs settings file
|
// custom javs settings file
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
String existingLocation =
|
||||||
if (!existing.isEmpty()) {
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
existing += ",";
|
if (!existingLocation.isEmpty()) {
|
||||||
|
existingLocation += ",";
|
||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existing + "file:configs/custom_settings.yml");
|
existingLocation + "file:configs/custom_settings.yml");
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,13 +126,12 @@ public class AppConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "directoryFilter")
|
@Bean(name = "directoryFilter")
|
||||||
public Predicate<Path> processPDFOnlyFilter() {
|
public Predicate<Path> processOnlyFiles() {
|
||||||
return path -> {
|
return path -> {
|
||||||
if (Files.isDirectory(path)) {
|
if (Files.isDirectory(path)) {
|
||||||
return !path.toString().contains("processing");
|
return !path.toString().contains("processing");
|
||||||
} else {
|
} else {
|
||||||
String fileName = path.getFileName().toString();
|
return true;
|
||||||
return fileName.endsWith(".pdf");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
"error",
|
"error",
|
||||||
"erroroauth",
|
"erroroauth",
|
||||||
"file",
|
"file",
|
||||||
"messageType");
|
"messageType",
|
||||||
|
"infoMessage");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
|
@ -31,25 +32,25 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Map<String, String> parameters = new HashMap<>();
|
Map<String, String> allowedParameters = new HashMap<>();
|
||||||
|
|
||||||
// Keep only the allowed parameters
|
// Keep only the allowed parameters
|
||||||
String[] queryParameters = queryString.split("&");
|
String[] queryParameters = queryString.split("&");
|
||||||
for (String param : queryParameters) {
|
for (String param : queryParameters) {
|
||||||
String[] keyValue = param.split("=");
|
String[] keyValuePair = param.split("=");
|
||||||
if (keyValue.length != 2) {
|
if (keyValuePair.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
if (ALLOWED_PARAMS.contains(keyValuePair[0])) {
|
||||||
parameters.put(keyValue[0], keyValue[1]);
|
allowedParameters.put(keyValuePair[0], keyValuePair[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any parameters that are not allowed
|
// If there are any parameters that are not allowed
|
||||||
if (parameters.size() != queryParameters.length) {
|
if (allowedParameters.size() != queryParameters.length) {
|
||||||
// Construct new query string
|
// Construct new query string
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
|
||||||
if (newQueryString.length() > 0) {
|
if (newQueryString.length() > 0) {
|
||||||
newQueryString.append("&");
|
newQueryString.append("&");
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,8 @@ import java.util.List;
|
||||||
|
|
||||||
import org.simpleyaml.configuration.comments.CommentType;
|
import org.simpleyaml.configuration.comments.CommentType;
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
|
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||||
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
|
@ -71,9 +73,17 @@ public class ConfigInitializer
|
||||||
}
|
}
|
||||||
|
|
||||||
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
||||||
|
DumperOptions yamlOptionsSettingsTemplateFile =
|
||||||
|
((SimpleYamlImplementation) settingsTemplateFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsSettingsTemplateFile.setSplitLines(false);
|
||||||
settingsTemplateFile.loadWithComments();
|
settingsTemplateFile.loadWithComments();
|
||||||
|
|
||||||
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
||||||
|
DumperOptions yamlOptionsSettingsFile =
|
||||||
|
((SimpleYamlImplementation) settingsFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsSettingsFile.setSplitLines(false);
|
||||||
settingsFile.loadWithComments();
|
settingsFile.loadWithComments();
|
||||||
|
|
||||||
// Load headers and comments
|
// Load headers and comments
|
||||||
|
@ -81,6 +91,10 @@ public class ConfigInitializer
|
||||||
|
|
||||||
// Create a new file for temporary settings
|
// Create a new file for temporary settings
|
||||||
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
||||||
|
DumperOptions yamlOptionsTempSettingFile =
|
||||||
|
((SimpleYamlImplementation) tempSettingFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsTempSettingFile.setSplitLines(false);
|
||||||
tempSettingFile.createNewFile(true);
|
tempSettingFile.createNewFile(true);
|
||||||
tempSettingFile.setHeader(header);
|
tempSettingFile.setHeader(header);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
|
public interface DatabaseBackupInterface {
|
||||||
|
void exportDatabase() throws IOException;
|
||||||
|
|
||||||
|
boolean importDatabase();
|
||||||
|
|
||||||
|
boolean hasBackup();
|
||||||
|
|
||||||
|
List<FileInfo> getBackupList();
|
||||||
|
}
|
|
@ -137,6 +137,7 @@ public class EndpointConfiguration {
|
||||||
addEndpointToGroup("Other", "auto-rename");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
|
addEndpointToGroup("Other", "remove-image-pdf");
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
|
@ -165,6 +166,7 @@ public class EndpointConfiguration {
|
||||||
addEndpointToGroup("Python", REMOVE_BLANKS);
|
addEndpointToGroup("Python", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
|
addEndpointToGroup("Python", "pdf-to-img");
|
||||||
|
|
||||||
// openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
|
@ -221,6 +223,7 @@ public class EndpointConfiguration {
|
||||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Java", "pdf-to-text");
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
|
addEndpointToGroup("Java", "remove-image-pdf");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
|
|
|
@ -3,9 +3,8 @@ package stirling.software.SPDF.config.security;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
@ -15,17 +14,16 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
|
||||||
|
|
||||||
public CustomAuthenticationFailureHandler(
|
public CustomAuthenticationFailureHandler(
|
||||||
final LoginAttemptService loginAttemptService, UserService userService) {
|
final LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
@ -39,14 +37,17 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (exception instanceof DisabledException) {
|
||||||
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
logger.error("Failed login attempt from IP: {}", ip);
|
log.error("Failed login attempt from IP: {}", ip);
|
||||||
|
|
||||||
String contextPath = request.getContextPath();
|
if (exception instanceof LockedException) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
||||||
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
|
||||||
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
|
||||||
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,20 +55,25 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
||||||
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
||||||
logger.info(
|
log.info(
|
||||||
"Remaining attempts for user {}: {}",
|
"Remaining attempts for user {}: {}",
|
||||||
optUser.get().getUsername(),
|
username,
|
||||||
loginAttemptService.getRemainingAttempts(username));
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
loginAttemptService.loginFailed(username);
|
loginAttemptService.loginFailed(username);
|
||||||
if (loginAttemptService.isBlocked(username)
|
if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
|
||||||
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
||||||
response.sendRedirect(contextPath + "/login?error=locked");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
if (exception instanceof BadCredentialsException
|
||||||
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
|| exception instanceof UsernameNotFoundException) {
|
||||||
response.sendRedirect(contextPath + "/login?error=badcredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof InternalAuthenticationServiceException
|
||||||
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,15 +10,20 @@ import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
|
public CustomAuthenticationSuccessHandler(
|
||||||
|
LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -27,6 +32,10 @@ public class CustomAuthenticationSuccessHandler
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
String userName = request.getParameter("username");
|
String userName = request.getParameter("username");
|
||||||
|
if (userService.isUserDisabled(userName)) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
loginAttemptService.loginSucceeded(userName);
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
|
|
|
@ -2,32 +2,26 @@ package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
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 jakarta.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
@Autowired SessionRegistry sessionRegistry;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
if (session != null) {
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
String sessionId = session.getId();
|
getRedirectStrategy()
|
||||||
sessionRegistry.removeSessionInformation(sessionId);
|
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
|
||||||
session.invalidate();
|
return;
|
||||||
logger.debug("Session invalidated: " + sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,28 +6,35 @@ import java.nio.file.Paths;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
import org.slf4j.Logger;
|
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@Slf4j
|
||||||
public class InitialSecuritySetup {
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class);
|
@Autowired private DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() throws IllegalArgumentException, IOException {
|
||||||
if (!userService.hasUsers()) {
|
if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
|
||||||
|
databaseBackupHelper.importDatabase();
|
||||||
|
} else if (!userService.hasUsers()) {
|
||||||
initializeAdminUser();
|
initializeAdminUser();
|
||||||
|
} else {
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
initializeInternalApiUser();
|
initializeInternalApiUser();
|
||||||
}
|
}
|
||||||
|
@ -41,12 +48,11 @@ public class InitialSecuritySetup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeAdminUser() {
|
private void initializeAdminUser() throws IOException {
|
||||||
String initialUsername =
|
String initialUsername =
|
||||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
String initialPassword =
|
String initialPassword =
|
||||||
applicationProperties.getSecurity().getInitialLogin().getPassword();
|
applicationProperties.getSecurity().getInitialLogin().getPassword();
|
||||||
|
|
||||||
if (initialUsername != null
|
if (initialUsername != null
|
||||||
&& !initialUsername.isEmpty()
|
&& !initialUsername.isEmpty()
|
||||||
&& initialPassword != null
|
&& initialPassword != null
|
||||||
|
@ -54,9 +60,9 @@ public class InitialSecuritySetup {
|
||||||
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
||||||
try {
|
try {
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||||
logger.info("Admin user created: " + initialUsername);
|
log.info("Admin user created: " + initialUsername);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
logger.error("Failed to initialize security setup", e);
|
log.error("Failed to initialize security setup", e);
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -64,23 +70,23 @@ public class InitialSecuritySetup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createDefaultAdminUser() {
|
private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
|
||||||
String defaultUsername = "admin";
|
String defaultUsername = "admin";
|
||||||
String defaultPassword = "stirling";
|
String defaultPassword = "stirling";
|
||||||
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
||||||
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
||||||
logger.info("Default admin user created: " + defaultUsername);
|
log.info("Default admin user created: " + defaultUsername);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeInternalApiUser() {
|
private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
|
||||||
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
|
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
userService.saveUser(
|
userService.saveUser(
|
||||||
Role.INTERNAL_API_USER.getRoleId(),
|
Role.INTERNAL_API_USER.getRoleId(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
Role.INTERNAL_API_USER.getRoleId());
|
Role.INTERNAL_API_USER.getRoleId());
|
||||||
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
logger.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +94,9 @@ public class InitialSecuritySetup {
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
||||||
|
yamlOptionssettingsYml.setSplitLines(false);
|
||||||
|
|
||||||
settingsYml.loadWithComments();
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
|
|
@ -3,29 +3,32 @@ 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.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.AttemptCounter;
|
import stirling.software.SPDF.model.AttemptCounter;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class LoginAttemptService {
|
public class LoginAttemptService {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
|
if (MAX_ATTEMPT == -1) {
|
||||||
|
isBlockedEnabled = false;
|
||||||
|
log.info("Login attempt tracking is disabled.");
|
||||||
|
}
|
||||||
ATTEMPT_INCREMENT_TIME =
|
ATTEMPT_INCREMENT_TIME =
|
||||||
TimeUnit.MINUTES.toMillis(
|
TimeUnit.MINUTES.toMillis(
|
||||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
|
@ -33,14 +36,16 @@ public class LoginAttemptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loginSucceeded(String key) {
|
public void loginSucceeded(String key) {
|
||||||
if (key == null || key.trim().isEmpty()) {
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attemptsCache.remove(key.toLowerCase());
|
attemptsCache.remove(key.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loginFailed(String key) {
|
public void loginFailed(String key) {
|
||||||
if (key == null || key.trim().isEmpty()) return;
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
|
@ -55,7 +60,9 @@ public class LoginAttemptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlocked(String key) {
|
public boolean isBlocked(String key) {
|
||||||
if (key == null || key.trim().isEmpty()) return false;
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -65,7 +72,9 @@ public class LoginAttemptService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRemainingAttempts(String key) {
|
public int getRemainingAttempts(String key) {
|
||||||
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT;
|
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
||||||
|
return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled
|
||||||
|
}
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
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.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
@ -18,8 +17,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
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;
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
|
@ -37,6 +34,7 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
|
@ -47,7 +45,7 @@ import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity()
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@ -73,11 +71,7 @@ public class SecurityConfiguration {
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
@Autowired private SessionPersistentRegistry sessionRegistry;
|
||||||
@Bean
|
|
||||||
public SessionRegistry sessionRegistry() {
|
|
||||||
return new SessionRegistryImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
@ -94,7 +88,7 @@ public class SecurityConfiguration {
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
.maximumSessions(10)
|
.maximumSessions(10)
|
||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry())
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
|
@ -103,7 +97,7 @@ public class SecurityConfiguration {
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler(
|
new CustomAuthenticationSuccessHandler(
|
||||||
loginAttemptService))
|
loginAttemptService, userService))
|
||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
|
@ -155,12 +149,15 @@ public class SecurityConfiguration {
|
||||||
})
|
})
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated())
|
.authenticated());
|
||||||
.authenticationProvider(authenticationProvider());
|
|
||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().getOAUTH2() != null
|
if (applicationProperties.getSecurity().getOAUTH2() != null
|
||||||
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()
|
||||||
|
&& !applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getLoginMethod()
|
||||||
|
.equalsIgnoreCase("normal")) {
|
||||||
|
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
|
@ -192,9 +189,7 @@ public class SecurityConfiguration {
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutSuccessHandler(
|
logout.logoutSuccessHandler(
|
||||||
new CustomOAuth2LogoutSuccessHandler(
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
this.applicationProperties,
|
applicationProperties)));
|
||||||
sessionRegistry()))
|
|
||||||
.invalidateHttpSession(true));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
|
@ -382,14 +377,6 @@ public class SecurityConfiguration {
|
||||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
|
||||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
|
||||||
authProvider.setUserDetailsService(userDetailsService);
|
|
||||||
authProvider.setPasswordEncoder(passwordEncoder());
|
|
||||||
return authProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl();
|
return new JPATokenRepositoryImpl();
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
@ -8,9 +11,11 @@ import org.springframework.context.annotation.Lazy;
|
||||||
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.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
@ -18,15 +23,17 @@ import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired private UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
@ -51,15 +58,20 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
try {
|
try {
|
||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
// provider for API keys.
|
// provider for API keys.
|
||||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||||
if (userDetails == null) {
|
if (!user.isPresent()) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
authentication =
|
List<SimpleGrantedAuthority> authorities =
|
||||||
new ApiKeyAuthenticationToken(
|
user.get().getAuthorities().stream()
|
||||||
userDetails, apiKey, userDetails.getAuthorities());
|
.map(
|
||||||
|
authority ->
|
||||||
|
new SimpleGrantedAuthority(
|
||||||
|
authority.getAuthority()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
// If API key authentication fails, deny the request
|
// If API key authentication fails, deny the request
|
||||||
|
@ -87,6 +99,43 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the authenticated user is disabled and invalidate their session if so
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username = null;
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
username = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
username = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
username = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
|
||||||
|
if (username != null) {
|
||||||
|
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
||||||
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
|
if (!isUserExists || isUserDisabled) {
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionsInformation.expireNow();
|
||||||
|
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUserExists) {
|
||||||
|
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isUserDisabled) {
|
||||||
|
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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;
|
||||||
|
@ -14,11 +7,14 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
||||||
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.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
|
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.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
@ -27,6 +23,10 @@ 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;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserService implements UserServiceInterface {
|
public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
|
@ -38,12 +38,17 @@ public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
@Autowired private MessageSource messageSource;
|
@Autowired private MessageSource messageSource;
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
|
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) {
|
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -55,8 +60,8 @@ public class UserService implements UserServiceInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
User user = getUserByApiKey(apiKey);
|
Optional<User> user = getUserByApiKey(apiKey);
|
||||||
if (user == null) {
|
if (!user.isPresent()) {
|
||||||
throw new UsernameNotFoundException("API key is not valid");
|
throw new UsernameNotFoundException("API key is not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +69,7 @@ public class UserService implements UserServiceInterface {
|
||||||
return new UsernamePasswordAuthenticationToken(
|
return new UsernamePasswordAuthenticationToken(
|
||||||
user, // principal (typically the user)
|
user, // principal (typically the user)
|
||||||
null, // credentials (we don't expose the password or API key here)
|
null, // credentials (we don't expose the password or API key here)
|
||||||
getAuthorities(user) // user's authorities (roles/permissions)
|
getAuthorities(user.get()) // user's authorities (roles/permissions)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,18 +84,17 @@ public class UserService implements UserServiceInterface {
|
||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
apiKey = UUID.randomUUID().toString();
|
apiKey = UUID.randomUUID().toString();
|
||||||
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
} while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user =
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
userRepository
|
if (user.isPresent()) {
|
||||||
.findByUsernameIgnoreCase(username)
|
user.get().setApiKey(generateApiKey());
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
return userRepository.save(user.get());
|
||||||
|
}
|
||||||
user.setApiKey(generateApiKey());
|
throw new UsernameNotFoundException("User not found");
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
|
@ -99,39 +103,40 @@ public class UserService implements UserServiceInterface {
|
||||||
|
|
||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
userRepository
|
findByUsernameIgnoreCase(username)
|
||||||
.findByUsernameIgnoreCase(username)
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValidApiKey(String apiKey) {
|
public boolean isValidApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey) != null;
|
return userRepository.findByApiKey(apiKey).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User getUserByApiKey(String apiKey) {
|
public Optional<User> getUserByApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public Optional<User> loadUserByApiKey(String apiKey) {
|
||||||
User user = userRepository.findByApiKey(apiKey);
|
Optional<User> user = userRepository.findByApiKey(apiKey);
|
||||||
if (user != null) {
|
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
if (user.isPresent()) {
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return user;
|
||||||
user.getUsername(),
|
|
||||||
user.getPassword(), // you might not need this for API key auth
|
|
||||||
getAuthorities(user));
|
|
||||||
}
|
}
|
||||||
return null; // or throw an exception
|
return null; // or throw an exception
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType)
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
throws IllegalArgumentException {
|
throws IllegalArgumentException, IOException {
|
||||||
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
|
@ -139,12 +144,14 @@ public class UserService implements UserServiceInterface {
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setFirstLogin(false);
|
user.setFirstLogin(false);
|
||||||
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setAuthenticationType(authenticationType);
|
user.setAuthenticationType(authenticationType);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password) throws IllegalArgumentException {
|
public void saveUser(String username, String password)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
|
@ -154,10 +161,11 @@ public class UserService implements UserServiceInterface {
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role, boolean firstLogin)
|
public void saveUser(String username, String password, String role, boolean firstLogin)
|
||||||
throws IllegalArgumentException {
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
|
@ -169,15 +177,16 @@ public class UserService implements UserServiceInterface {
|
||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
user.setFirstLogin(firstLogin);
|
user.setFirstLogin(firstLogin);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role)
|
public void saveUser(String username, String password, String role)
|
||||||
throws IllegalArgumentException {
|
throws IllegalArgumentException, IOException {
|
||||||
saveUser(username, password, role, false);
|
saveUser(username, password, role, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUser(String username) {
|
public void deleteUser(String username) {
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
for (Authority authority : userOpt.get().getAuthorities()) {
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
|
@ -186,28 +195,28 @@ public class UserService implements UserServiceInterface {
|
||||||
}
|
}
|
||||||
userRepository.delete(userOpt.get());
|
userRepository.delete(userOpt.get());
|
||||||
}
|
}
|
||||||
|
invalidateUserSessions(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExists(String username) {
|
public boolean usernameExists(String username) {
|
||||||
return userRepository.findByUsername(username).isPresent();
|
return findByUsername(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExistsIgnoreCase(String username) {
|
public boolean usernameExistsIgnoreCase(String username) {
|
||||||
return userRepository.findByUsernameIgnoreCase(username).isPresent();
|
return findByUsernameIgnoreCase(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasUsers() {
|
public boolean hasUsers() {
|
||||||
long userCount = userRepository.count();
|
long userCount = userRepository.count();
|
||||||
if (userRepository
|
if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
|
||||||
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
|
|
||||||
.isPresent()) {
|
|
||||||
userCount -= 1;
|
userCount -= 1;
|
||||||
}
|
}
|
||||||
return userCount > 0;
|
return userCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUserSettings(String username, Map<String, String> updates) {
|
public void updateUserSettings(String username, Map<String, String> updates)
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
throws IOException {
|
||||||
|
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
||||||
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();
|
||||||
|
@ -220,6 +229,7 @@ public class UserService implements UserServiceInterface {
|
||||||
user.setSettings(settingsMap);
|
user.setSettings(settingsMap);
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,32 +241,47 @@ public class UserService implements UserServiceInterface {
|
||||||
return userRepository.findByUsernameIgnoreCase(username);
|
return userRepository.findByUsernameIgnoreCase(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<User> findByUsernameIgnoreCaseWithSettings(String username) {
|
||||||
|
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||||
|
}
|
||||||
|
|
||||||
public Authority findRole(User user) {
|
public Authority findRole(User user) {
|
||||||
return authorityRepository.findByUserId(user.getId());
|
return authorityRepository.findByUserId(user.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeUsername(User user, String newUsername) throws IllegalArgumentException {
|
public void changeUsername(User user, String newUsername)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(newUsername)) {
|
if (!isUsernameValid(newUsername)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
user.setUsername(newUsername);
|
user.setUsername(newUsername);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(User user, String newPassword) {
|
public void changePassword(User user, String newPassword) throws IOException {
|
||||||
user.setPassword(passwordEncoder.encode(newPassword));
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeFirstUse(User user, boolean firstUse) {
|
public void changeFirstUse(User user, boolean firstUse) throws IOException {
|
||||||
user.setFirstLogin(firstUse);
|
user.setFirstLogin(firstUse);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeRole(User user, String newRole) {
|
public void changeRole(User user, String newRole) throws IOException {
|
||||||
Authority userAuthority = this.findRole(user);
|
Authority userAuthority = this.findRole(user);
|
||||||
userAuthority.setAuthority(newRole);
|
userAuthority.setAuthority(newRole);
|
||||||
authorityRepository.save(userAuthority);
|
authorityRepository.save(userAuthority);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
|
||||||
|
user.setEnabled(enbeled);
|
||||||
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
|
@ -280,14 +305,40 @@ public class UserService implements UserServiceInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasPassword(String username) {
|
public boolean hasPassword(String username) {
|
||||||
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent() && user.get().hasPassword();
|
return user.isPresent() && user.get().hasPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAuthenticationTypeByUsername(
|
public boolean isAuthenticationTypeByUsername(
|
||||||
String username, AuthenticationType authenticationType) {
|
String username, AuthenticationType authenticationType) {
|
||||||
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent()
|
return user.isPresent()
|
||||||
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUserDisabled(String username) {
|
||||||
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
|
return userOpt.map(user -> !user.isEnabled()).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidateUserSessions(String username) {
|
||||||
|
String usernameP = "";
|
||||||
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
|
for (SessionInformation sessionsInformation :
|
||||||
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
usernameP = userDetails.getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||||
|
usernameP = oAuth2User.getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
usernameP = (String) principal;
|
||||||
|
}
|
||||||
|
if (usernameP.equalsIgnoreCase(username)) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,205 @@
|
||||||
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.sql.Connection;
|
||||||
|
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.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||||
|
|
||||||
|
@Value("${spring.datasource.url}")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
private Path backupPath = Paths.get("configs/db/backup/");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasBackup() {
|
||||||
|
// Check if there is at least one backup
|
||||||
|
return !getBackupList().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<FileInfo> getBackupList() {
|
||||||
|
// Check if the backup directory exists, and create it if it does not
|
||||||
|
ensureBackupDirectoryExists();
|
||||||
|
|
||||||
|
List<FileInfo> backupFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
// Read the backup directory and filter for files with the prefix "backup_" and suffix
|
||||||
|
// ".sql"
|
||||||
|
try (DirectoryStream<Path> stream =
|
||||||
|
Files.newDirectoryStream(
|
||||||
|
backupPath,
|
||||||
|
path ->
|
||||||
|
path.getFileName().toString().startsWith("backup_")
|
||||||
|
&& path.getFileName().toString().endsWith(".sql"))) {
|
||||||
|
for (Path entry : stream) {
|
||||||
|
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
|
||||||
|
LocalDateTime modificationDate =
|
||||||
|
LocalDateTime.ofInstant(
|
||||||
|
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
|
||||||
|
LocalDateTime creationDate =
|
||||||
|
LocalDateTime.ofInstant(
|
||||||
|
attrs.creationTime().toInstant(), ZoneId.systemDefault());
|
||||||
|
long fileSize = attrs.size();
|
||||||
|
backupFiles.add(
|
||||||
|
new FileInfo(
|
||||||
|
entry.getFileName().toString(),
|
||||||
|
entry.toString(),
|
||||||
|
modificationDate,
|
||||||
|
fileSize,
|
||||||
|
creationDate));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error reading backup directory: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return backupFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imports a database backup from the specified file.
|
||||||
|
public boolean importDatabaseFromUI(String fileName) throws IOException {
|
||||||
|
return this.importDatabaseFromUI(getBackupFilePath(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imports a database backup from the specified path.
|
||||||
|
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
|
||||||
|
boolean success = executeDatabaseScript(tempTemplatePath);
|
||||||
|
if (success) {
|
||||||
|
LocalDateTime dateNow = LocalDateTime.now();
|
||||||
|
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||||
|
Path insertOutputFilePath =
|
||||||
|
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
|
||||||
|
Files.copy(tempTemplatePath, insertOutputFilePath);
|
||||||
|
Files.deleteIfExists(tempTemplatePath);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean importDatabase() {
|
||||||
|
if (!this.hasBackup()) return false;
|
||||||
|
|
||||||
|
List<FileInfo> backupList = this.getBackupList();
|
||||||
|
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
||||||
|
|
||||||
|
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportDatabase() throws IOException {
|
||||||
|
// Check if the backup directory exists, and create it if it does not
|
||||||
|
ensureBackupDirectoryExists();
|
||||||
|
|
||||||
|
// Filter and delete old backups if there are more than 5
|
||||||
|
List<FileInfo> filteredBackupList =
|
||||||
|
this.getBackupList().stream()
|
||||||
|
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (filteredBackupList.size() > 5) {
|
||||||
|
filteredBackupList.sort(
|
||||||
|
Comparator.comparing(
|
||||||
|
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
||||||
|
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
|
||||||
|
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime dateNow = LocalDateTime.now();
|
||||||
|
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||||
|
Path insertOutputFilePath =
|
||||||
|
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||||
|
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
stmt.setString(1, insertOutputFilePath.toString());
|
||||||
|
stmt.execute();
|
||||||
|
log.info("Database export completed: {}", insertOutputFilePath);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error during database export: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieves the H2 database version.
|
||||||
|
public String getH2Version() {
|
||||||
|
String version = "Unknown";
|
||||||
|
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
|
||||||
|
try (Statement stmt = conn.createStatement();
|
||||||
|
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||||
|
if (rs.next()) {
|
||||||
|
version = rs.getString("version");
|
||||||
|
log.info("H2 Database Version: {}", version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes a backup file.
|
||||||
|
public boolean deleteBackupFile(String fileName) throws IOException {
|
||||||
|
Path filePath = this.getBackupFilePath(fileName);
|
||||||
|
if (Files.deleteIfExists(filePath)) {
|
||||||
|
log.info("Deleted backup file: {}", fileName);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.error("File not found or could not be deleted: {}", fileName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gets the Path object for a given backup file name.
|
||||||
|
public Path getBackupFilePath(String fileName) {
|
||||||
|
return Paths.get(backupPath.toString(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean executeDatabaseScript(Path scriptPath) {
|
||||||
|
String query = "RUNSCRIPT from ?;";
|
||||||
|
|
||||||
|
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
stmt.setString(1, scriptPath.toString());
|
||||||
|
stmt.execute();
|
||||||
|
log.info("Database import completed: {}", scriptPath);
|
||||||
|
return true;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error during database import: {}", e.getMessage(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureBackupDirectoryExists() {
|
||||||
|
if (Files.notExists(backupPath)) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(backupPath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error creating directories: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ScheduledTasks {
|
||||||
|
|
||||||
|
@Autowired private DatabaseBackupHelper databaseBackupService;
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
|
public void performBackup() throws IOException {
|
||||||
|
databaseBackupService.exportDatabase();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,8 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.springframework.security.authentication.DisabledException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
@ -13,19 +13,34 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomOAuth2AuthenticationFailureHandler
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
extends SimpleUrlAuthenticationFailureHandler {
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (exception instanceof BadCredentialsException) {
|
||||||
|
log.error("BadCredentialsException", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof DisabledException) {
|
||||||
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof LockedException) {
|
||||||
|
log.error("Account locked: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (exception instanceof OAuth2AuthenticationException) {
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
|
@ -34,17 +49,13 @@ public class CustomOAuth2AuthenticationFailureHandler
|
||||||
if (error.getErrorCode().equals("Password must not be null")) {
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
errorCode = "userAlreadyExistsWeb";
|
errorCode = "userAlreadyExistsWeb";
|
||||||
}
|
}
|
||||||
logger.error("OAuth2 Authentication error: " + errorCode);
|
log.error("OAuth2 Authentication error: " + errorCode);
|
||||||
|
log.error("OAuth2AuthenticationException", exception);
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
return;
|
return;
|
||||||
} else if (exception instanceof LockedException) {
|
}
|
||||||
logger.error("Account locked: ", exception);
|
log.error("Unhandled authentication exception", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
logger.error("Unhandled authentication exception", exception);
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,9 @@ package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
@ -26,9 +25,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
|
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
private ApplicationProperties applicationProperties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
@ -46,6 +42,17 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username = "";
|
||||||
|
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
|
OAuth2User oauthUser = (OAuth2User) principal;
|
||||||
|
username = oauthUser.getName();
|
||||||
|
} else if (principal instanceof UserDetails) {
|
||||||
|
UserDetails oauthUser = (UserDetails) principal;
|
||||||
|
username = oauthUser.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
|
@ -59,11 +66,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
|
||||||
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
String username = oauthUser.getName();
|
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
|
@ -78,9 +82,16 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
try {
|
try {
|
||||||
|
if (oAuth.getBlockRegistration()
|
||||||
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
}
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
return;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
@ -89,5 +100,4 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,34 +2,26 @@ package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
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 jakarta.servlet.http.HttpSession;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.Provider;
|
import stirling.software.SPDF.model.Provider;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
|
|
||||||
|
|
||||||
private final SessionRegistry sessionRegistry;
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public CustomOAuth2LogoutSuccessHandler(
|
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
|
||||||
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
|
|
||||||
this.sessionRegistry = sessionRegistry;
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +34,15 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||||
String issuer = null;
|
String issuer = null;
|
||||||
String clientId = null;
|
String clientId = null;
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
||||||
|
} else {
|
||||||
|
super.onLogoutSuccess(request, response, authentication);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
|
@ -53,9 +54,8 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||||
issuer = provider.getIssuer();
|
issuer = provider.getIssuer();
|
||||||
clientId = provider.getClientId();
|
clientId = provider.getClientId();
|
||||||
} catch (UnsupportedProviderException e) {
|
} catch (UnsupportedProviderException e) {
|
||||||
logger.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
issuer = oauth.getIssuer();
|
issuer = oauth.getIssuer();
|
||||||
|
@ -70,18 +70,16 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
||||||
|
param = "erroroauth=oauth2_admin_blocked_user";
|
||||||
|
} else if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
param = "erroroauth=userIsDisabled";
|
||||||
|
} else if (request.getParameter("badcredentials") != null) {
|
||||||
|
param = "error=badcredentials";
|
||||||
}
|
}
|
||||||
|
|
||||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||||
|
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
if (session != null) {
|
|
||||||
String sessionId = session.getId();
|
|
||||||
sessionRegistry.removeSessionInformation(sessionId);
|
|
||||||
session.invalidate();
|
|
||||||
logger.info("Session invalidated: " + sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
// Add Keycloak specific logout URL if needed
|
// Add Keycloak specific logout URL if needed
|
||||||
|
@ -92,13 +90,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||||
+ clientId
|
+ clientId
|
||||||
+ "&post_logout_redirect_uri="
|
+ "&post_logout_redirect_uri="
|
||||||
+ response.encodeRedirectURL(redirect_url);
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
break;
|
break;
|
||||||
case "github":
|
case "github":
|
||||||
// Add GitHub specific logout URL if needed
|
// Add GitHub specific logout URL if needed
|
||||||
String githubLogoutUrl = "https://github.com/logout";
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
response.sendRedirect(githubLogoutUrl);
|
response.sendRedirect(githubLogoutUrl);
|
||||||
break;
|
break;
|
||||||
case "google":
|
case "google":
|
||||||
|
@ -106,13 +104,14 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
||||||
// String googleLogoutUrl =
|
// String googleLogoutUrl =
|
||||||
// "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);
|
||||||
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
log.info("Google does not have a specific logout URL");
|
||||||
|
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
// break;
|
// break;
|
||||||
default:
|
default:
|
||||||
String redirectUrl = request.getContextPath() + "/login?" + param;
|
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||||
response.sendRedirect(redirectUrl);
|
response.sendRedirect(defaultRedirectUrl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class CustomHttpSessionListener implements HttpSessionListener {
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionCreated(HttpSessionEvent se) {
|
||||||
|
log.info("Session created: " + se.getSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
|
log.info("Session destroyed: " + se.getSession().getId());
|
||||||
|
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
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.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class SessionPersistentRegistry implements SessionRegistry {
|
||||||
|
|
||||||
|
private final SessionRepository sessionRepository;
|
||||||
|
|
||||||
|
@Value("${server.servlet.session.timeout:30m}")
|
||||||
|
private Duration defaultMaxInactiveInterval;
|
||||||
|
|
||||||
|
public SessionPersistentRegistry(SessionRepository sessionRepository) {
|
||||||
|
this.sessionRepository = sessionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getAllPrincipals() {
|
||||||
|
List<SessionEntity> sessions = sessionRepository.findAll();
|
||||||
|
List<Object> principals = new ArrayList<>();
|
||||||
|
for (SessionEntity session : sessions) {
|
||||||
|
principals.add(session.getPrincipalName());
|
||||||
|
}
|
||||||
|
return principals;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SessionInformation> getAllSessions(
|
||||||
|
Object principal, boolean includeExpiredSessions) {
|
||||||
|
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||||
|
String principalName = null;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
principalName = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
principalName = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principalName != null) {
|
||||||
|
List<SessionEntity> sessionEntities =
|
||||||
|
sessionRepository.findByPrincipalName(principalName);
|
||||||
|
for (SessionEntity sessionEntity : sessionEntities) {
|
||||||
|
if (includeExpiredSessions || !sessionEntity.isExpired()) {
|
||||||
|
sessionInformations.add(
|
||||||
|
new SessionInformation(
|
||||||
|
sessionEntity.getPrincipalName(),
|
||||||
|
sessionEntity.getSessionId(),
|
||||||
|
sessionEntity.getLastRequest()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessionInformations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void registerNewSession(String sessionId, Object principal) {
|
||||||
|
String principalName = null;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
principalName = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
principalName = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principalName != null) {
|
||||||
|
SessionEntity sessionEntity = new SessionEntity();
|
||||||
|
sessionEntity.setSessionId(sessionId);
|
||||||
|
sessionEntity.setPrincipalName(principalName);
|
||||||
|
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
||||||
|
sessionEntity.setExpired(false);
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void removeSessionInformation(String sessionId) {
|
||||||
|
sessionRepository.deleteById(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void refreshLastRequest(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionInformation getSessionInformation(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
return new SessionInformation(
|
||||||
|
sessionEntity.getPrincipalName(),
|
||||||
|
sessionEntity.getSessionId(),
|
||||||
|
sessionEntity.getLastRequest());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all non-expired sessions
|
||||||
|
public List<SessionEntity> getAllSessionsNotExpired() {
|
||||||
|
return sessionRepository.findByExpired(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all sessions
|
||||||
|
public List<SessionEntity> getAllSessions() {
|
||||||
|
return sessionRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a session as expired
|
||||||
|
public void expireSession(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
sessionEntity.setExpired(true); // Set expired to true
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the maximum inactive interval for sessions
|
||||||
|
public int getMaxInactiveInterval() {
|
||||||
|
return (int) defaultMaxInactiveInterval.getSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a session entity by session ID
|
||||||
|
public SessionEntity getSessionEntity(String sessionId) {
|
||||||
|
return sessionRepository.findBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session details by principal name
|
||||||
|
public void updateSessionByPrincipalName(
|
||||||
|
String principalName, boolean expired, Date lastRequest) {
|
||||||
|
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest session for a given principal name
|
||||||
|
public Optional<SessionEntity> findLatestSession(String principalName) {
|
||||||
|
List<SessionEntity> allSessions = sessionRepository.findByPrincipalName(principalName);
|
||||||
|
if (allSessions.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sessions by lastRequest in descending order
|
||||||
|
Collections.sort(
|
||||||
|
allSessions,
|
||||||
|
new Comparator<SessionEntity>() {
|
||||||
|
@Override
|
||||||
|
public int compare(SessionEntity s1, SessionEntity s2) {
|
||||||
|
// Sort by lastRequest in descending order
|
||||||
|
return s2.getLastRequest().compareTo(s1.getLastRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The first session in the list is the latest session for the given principal name
|
||||||
|
return Optional.of(allSessions.get(0));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SessionRegistryConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionRegistryImpl sessionRegistry() {
|
||||||
|
return new SessionRegistryImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionPersistentRegistry sessionPersistentRegistry(
|
||||||
|
SessionRepository sessionRepository) {
|
||||||
|
return new SessionPersistentRegistry(sessionRepository);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SessionRepository extends JpaRepository<SessionEntity, String> {
|
||||||
|
List<SessionEntity> findByPrincipalName(String principalName);
|
||||||
|
|
||||||
|
List<SessionEntity> findByExpired(boolean expired);
|
||||||
|
|
||||||
|
SessionEntity findBySessionId(String sessionId);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query(
|
||||||
|
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
|
||||||
|
void saveByPrincipalName(
|
||||||
|
@Param("expired") boolean expired,
|
||||||
|
@Param("lastRequest") Date lastRequest,
|
||||||
|
@Param("principalName") String principalName);
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class SessionScheduled {
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0/5 * * * ?")
|
||||||
|
public void expireSessions() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
||||||
|
List<SessionInformation> sessionInformations =
|
||||||
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
for (SessionInformation sessionInformation : sessionInformations) {
|
||||||
|
Date lastRequest = sessionInformation.getLastRequest();
|
||||||
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
|
Instant expirationTime =
|
||||||
|
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
|
if (now.isAfter(expirationTime)) {
|
||||||
|
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.io.InputStreamResource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
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.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
@RequestMapping("/api/v1/database")
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Tag(name = "Database", description = "Database APIs")
|
||||||
|
public class DatabaseController {
|
||||||
|
|
||||||
|
@Autowired DatabaseBackupHelper databaseBackupHelper;
|
||||||
|
|
||||||
|
@Hidden
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "import-database")
|
||||||
|
@Operation(
|
||||||
|
summary = "Import database backup",
|
||||||
|
description = "This endpoint imports a database backup from a SQL file.")
|
||||||
|
public String importDatabase(
|
||||||
|
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
|
||||||
|
return "redirect:/database";
|
||||||
|
}
|
||||||
|
log.info("Received file: {}", file.getOriginalFilename());
|
||||||
|
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
|
||||||
|
try (InputStream in = file.getInputStream()) {
|
||||||
|
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
|
||||||
|
if (importSuccess) {
|
||||||
|
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
|
||||||
|
} else {
|
||||||
|
redirectAttributes.addAttribute("error", "failedImportFile");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error importing database: {}", e.getMessage());
|
||||||
|
redirectAttributes.addAttribute("error", "failedImportFile");
|
||||||
|
}
|
||||||
|
return "redirect:/database";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hidden
|
||||||
|
@GetMapping("/import-database-file/{fileName}")
|
||||||
|
public String importDatabaseFromBackupUI(@PathVariable String fileName)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
|
return "redirect:/database?error=fileNullOrEmpty";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the file exists in the backup list
|
||||||
|
boolean fileExists =
|
||||||
|
databaseBackupHelper.getBackupList().stream()
|
||||||
|
.anyMatch(backup -> backup.getFileName().equals(fileName));
|
||||||
|
if (!fileExists) {
|
||||||
|
log.error("File {} not found in backup list", fileName);
|
||||||
|
return "redirect:/database?error=fileNotFound";
|
||||||
|
}
|
||||||
|
log.info("Received file: {}", fileName);
|
||||||
|
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
|
||||||
|
log.info("File {} imported to database", fileName);
|
||||||
|
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
|
||||||
|
}
|
||||||
|
return "redirect:/database?error=failedImportFile";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hidden
|
||||||
|
@GetMapping("/delete/{fileName}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Delete a database backup file",
|
||||||
|
description =
|
||||||
|
"This endpoint deletes a database backup file with the specified file name.")
|
||||||
|
public String deleteFile(@PathVariable String fileName) {
|
||||||
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (databaseBackupHelper.deleteBackupFile(fileName)) {
|
||||||
|
log.info("Deleted file: {}", fileName);
|
||||||
|
} else {
|
||||||
|
log.error("Failed to delete file: {}", fileName);
|
||||||
|
return "redirect:/database?error=failedToDeleteFile";
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error deleting file: {}", e.getMessage());
|
||||||
|
return "redirect:/database?error=" + e.getMessage();
|
||||||
|
}
|
||||||
|
return "redirect:/database";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Hidden
|
||||||
|
@GetMapping("/download/{fileName}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Download a database backup file",
|
||||||
|
description =
|
||||||
|
"This endpoint downloads a database backup file with the specified file name.")
|
||||||
|
public ResponseEntity<?> downloadFile(@PathVariable String fileName) {
|
||||||
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
|
||||||
|
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.contentLength(Files.size(filePath))
|
||||||
|
.body(resource);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error downloading file: {}", e.getMessage());
|
||||||
|
return ResponseEntity.status(HttpStatus.SEE_OTHER_303)
|
||||||
|
.location(URI.create("/database?error=downloadFailed"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
import stirling.software.SPDF.service.PdfImageRemovalService;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
|
||||||
|
* from a PDF file to reduce its size.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
public class PdfImageRemovalController {
|
||||||
|
|
||||||
|
// Service for removing images from PDFs
|
||||||
|
@Autowired private PdfImageRemovalService pdfImageRemovalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for dependency injection of PdfImageRemovalService.
|
||||||
|
*
|
||||||
|
* @param pdfImageRemovalService The service used for removing images from PDFs.
|
||||||
|
*/
|
||||||
|
public PdfImageRemovalController(PdfImageRemovalService pdfImageRemovalService) {
|
||||||
|
this.pdfImageRemovalService = pdfImageRemovalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to remove images from a PDF file.
|
||||||
|
*
|
||||||
|
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
|
||||||
|
* PDF file with a new name indicating that images were removed.
|
||||||
|
*
|
||||||
|
* @param file The PDF file with images to be removed.
|
||||||
|
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
|
||||||
|
* content type and filename.
|
||||||
|
* @throws IOException If an error occurs while processing the PDF file.
|
||||||
|
*/
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Remove images from file to reduce the file size.",
|
||||||
|
description =
|
||||||
|
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
|
||||||
|
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
|
||||||
|
|
||||||
|
MultipartFile pdf = file.getFileInput();
|
||||||
|
|
||||||
|
// Convert the MultipartFile to a byte array
|
||||||
|
byte[] pdfBytes = pdf.getBytes();
|
||||||
|
|
||||||
|
// Load the PDF document from the byte array
|
||||||
|
PDDocument document = Loader.loadPDF(pdfBytes);
|
||||||
|
|
||||||
|
// Remove images from the PDF document using the service
|
||||||
|
PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
|
||||||
|
|
||||||
|
// Create a ByteArrayOutputStream to hold the modified PDF data
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// Save the modified PDF document to the output stream
|
||||||
|
modifiedDocument.save(outputStream);
|
||||||
|
modifiedDocument.close();
|
||||||
|
|
||||||
|
// Generate a new filename for the modified PDF
|
||||||
|
String mergedFileName =
|
||||||
|
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_images.pdf";
|
||||||
|
|
||||||
|
// Convert the byte array to a web response and return it
|
||||||
|
return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package stirling.software.SPDF.controller.api;
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -11,8 +13,8 @@ import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
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;
|
||||||
|
@ -29,6 +31,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
|
@ -40,9 +44,12 @@ public class UserController {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
|
@Autowired SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) {
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
||||||
|
throws IOException {
|
||||||
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
||||||
model.addAttribute("error", "Username already exists");
|
model.addAttribute("error", "Username already exists");
|
||||||
return "register";
|
return "register";
|
||||||
|
@ -63,7 +70,8 @@ public class UserController {
|
||||||
@RequestParam(name = "newUsername") String newUsername,
|
@RequestParam(name = "newUsername") String newUsername,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
if (!userService.isUsernameValid(newUsername)) {
|
if (!userService.isUsernameValid(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
|
@ -116,7 +124,8 @@ public class UserController {
|
||||||
@RequestParam(name = "newPassword") String newPassword,
|
@RequestParam(name = "newPassword") String newPassword,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes)
|
||||||
|
throws IOException {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
@ -149,7 +158,8 @@ public class UserController {
|
||||||
@RequestParam(name = "newPassword") String newPassword,
|
@RequestParam(name = "newPassword") String newPassword,
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes)
|
||||||
|
throws IOException {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
@ -176,7 +186,8 @@ public class UserController {
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/updateUserSettings")
|
@PostMapping("/updateUserSettings")
|
||||||
public String updateUserSettings(HttpServletRequest request, Principal principal) {
|
public String updateUserSettings(HttpServletRequest request, Principal principal)
|
||||||
|
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<>();
|
||||||
|
|
||||||
|
@ -197,11 +208,13 @@ public class UserController {
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam String username,
|
||||||
@RequestParam(name = "password") String password,
|
@RequestParam(name = "password", required = false) String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
|
@RequestParam(name = "authType") String authType,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange) {
|
boolean forceChange)
|
||||||
|
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);
|
||||||
|
@ -230,7 +243,15 @@ public class UserController {
|
||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
||||||
|
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
||||||
|
} else {
|
||||||
|
if (password.isBlank()) {
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||||
|
}
|
||||||
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", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
@ -240,7 +261,8 @@ public class UserController {
|
||||||
public RedirectView changeRole(
|
public RedirectView changeRole(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
Authentication authentication) {
|
Authentication authentication)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
@ -271,6 +293,60 @@ public class UserController {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
|
|
||||||
|
return new RedirectView(
|
||||||
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/changeUserEnabled/{username}")
|
||||||
|
public RedirectView changeUserEnabled(
|
||||||
|
@PathVariable("username") String username,
|
||||||
|
@RequestParam("enabled") boolean enabled,
|
||||||
|
Authentication authentication)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
if (!userOpt.isPresent()) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
|
}
|
||||||
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
|
}
|
||||||
|
// Get the currently authenticated username
|
||||||
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
|
// Check if the provided username matches the current session's username
|
||||||
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
||||||
|
}
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
userService.changeUserEnabled(user, enabled);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
// Invalidate all sessions if the user is being disabled
|
||||||
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
|
String userNameP = "";
|
||||||
|
for (Object principal : principals) {
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionRegistry.getAllSessions(principal, false);
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
userNameP = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
userNameP = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
userNameP = (String) principal;
|
||||||
|
}
|
||||||
|
if (userNameP.equalsIgnoreCase(username)) {
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
@ -278,7 +354,7 @@ public class UserController {
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public RedirectView deleteUser(
|
public RedirectView deleteUser(
|
||||||
@PathVariable(name = "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);
|
||||||
|
@ -291,27 +367,18 @@ public class UserController {
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||||
}
|
}
|
||||||
invalidateUserSessions(username);
|
|
||||||
|
// Invalidate all sessions before deleting the user
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return new RedirectView("/addUsers", true);
|
return new RedirectView("/addUsers", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired private SessionRegistry sessionRegistry;
|
|
||||||
|
|
||||||
private void invalidateUserSessions(String username) {
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
|
||||||
if (userDetails.getUsername().equals(username)) {
|
|
||||||
for (SessionInformation session :
|
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
|
||||||
session.expireNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/get-api-key")
|
@PostMapping("/get-api-key")
|
||||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -20,7 +30,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
|
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -60,15 +73,87 @@ public class ConvertImgPDFController {
|
||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
imageFormat.toUpperCase(),
|
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
|
||||||
colorTypeResult,
|
colorTypeResult,
|
||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
filename);
|
filename);
|
||||||
|
|
||||||
if (result == null || result.length == 0) {
|
if (result == null || result.length == 0) {
|
||||||
logger.error("resultant bytes for {} is null, error converting ", filename);
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
|
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
|
||||||
|
throw new IOException("Python is not installed. Required for WebP conversion.");
|
||||||
|
} else if (imageFormat.equalsIgnoreCase("webp")
|
||||||
|
&& CheckProgramInstall.isPythonAvailable()) {
|
||||||
|
// Write the output stream to a temp file
|
||||||
|
Path tempFile = Files.createTempFile("temp_png", ".png");
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
||||||
|
fos.write(result);
|
||||||
|
fos.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(pythonVersion);
|
||||||
|
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||||
|
|
||||||
|
// Create a temporary directory for the output WebP files
|
||||||
|
Path tempOutputDir = Files.createTempDirectory("webp_output");
|
||||||
|
if (singleImage) {
|
||||||
|
// Run the Python script to convert PNG to WebP
|
||||||
|
command.add(tempFile.toString());
|
||||||
|
command.add(tempOutputDir.toString());
|
||||||
|
command.add("--single");
|
||||||
|
} else {
|
||||||
|
// Save the uploaded PDF to a temporary file
|
||||||
|
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
|
||||||
|
file.transferTo(tempPdfPath.toFile());
|
||||||
|
// Run the Python script to convert PDF to WebP
|
||||||
|
command.add(tempPdfPath.toString());
|
||||||
|
command.add(tempOutputDir.toString());
|
||||||
|
}
|
||||||
|
command.add("--dpi");
|
||||||
|
command.add(dpi);
|
||||||
|
ProcessExecutorResult resultProcess =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
// Find all WebP files in the output directory
|
||||||
|
List<Path> webpFiles =
|
||||||
|
Files.walk(tempOutputDir)
|
||||||
|
.filter(path -> path.toString().endsWith(".webp"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (webpFiles.isEmpty()) {
|
||||||
|
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
|
||||||
|
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bodyBytes = new byte[0];
|
||||||
|
|
||||||
|
if (webpFiles.size() == 1) {
|
||||||
|
// Return the single WebP file directly
|
||||||
|
Path webpFilePath = webpFiles.get(0);
|
||||||
|
bodyBytes = Files.readAllBytes(webpFilePath);
|
||||||
|
} else {
|
||||||
|
// Create a ZIP file containing all WebP images
|
||||||
|
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
|
||||||
|
for (Path webpFile : webpFiles) {
|
||||||
|
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
|
||||||
|
Files.copy(webpFile, zos);
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyBytes = zipOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
// Clean up the temporary files
|
||||||
|
Files.deleteIfExists(tempFile);
|
||||||
|
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||||
|
result = bodyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
String docName = filename + "." + imageFormat;
|
String docName = filename + "." + imageFormat;
|
||||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||||
|
|
|
@ -39,6 +39,12 @@ public class ConvertWebsiteToPDF {
|
||||||
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate the URL is reachable
|
||||||
|
if (!GeneralUtils.isURLReachable(URL)) {
|
||||||
|
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
|
||||||
|
}
|
||||||
|
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -33,7 +33,7 @@ public class AutoRenameController {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
||||||
|
|
||||||
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
||||||
private static final int LINE_LIMIT = 11;
|
private static final int LINE_LIMIT = 200;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
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.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.stream.IntStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
@ -17,6 +17,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
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;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
@ -50,31 +51,31 @@ public class BlankPageController {
|
||||||
int threshold = request.getThreshold();
|
int threshold = request.getThreshold();
|
||||||
float whitePercent = request.getWhitePercent();
|
float whitePercent = request.getWhitePercent();
|
||||||
|
|
||||||
PDDocument document = null;
|
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
|
||||||
try {
|
|
||||||
document = Loader.loadPDF(inputFile.getBytes());
|
|
||||||
PDPageTree pages = document.getDocumentCatalog().getPages();
|
PDPageTree pages = document.getDocumentCatalog().getPages();
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
|
|
||||||
List<Integer> pagesToKeepIndex = new ArrayList<>();
|
List<PDPage> nonBlankPages = new ArrayList<>();
|
||||||
|
List<PDPage> blankPages = new ArrayList<>();
|
||||||
int pageIndex = 0;
|
int pageIndex = 0;
|
||||||
|
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (PDPage page : pages) {
|
for (PDPage page : pages) {
|
||||||
logger.info("checking page " + pageIndex);
|
logger.info("checking page {}", pageIndex);
|
||||||
textStripper.setStartPage(pageIndex + 1);
|
textStripper.setStartPage(pageIndex + 1);
|
||||||
textStripper.setEndPage(pageIndex + 1);
|
textStripper.setEndPage(pageIndex + 1);
|
||||||
String pageText = textStripper.getText(document);
|
String pageText = textStripper.getText(document);
|
||||||
boolean hasText = !pageText.trim().isEmpty();
|
boolean hasText = !pageText.trim().isEmpty();
|
||||||
|
|
||||||
Boolean blank = true;
|
boolean blank = true;
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
logger.info("page " + pageIndex + " has text, not blank");
|
logger.info("page {} has text, not blank", pageIndex);
|
||||||
blank = false;
|
blank = false;
|
||||||
} else {
|
} else {
|
||||||
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
logger.info("page " + pageIndex + " has image, running blank detection");
|
logger.info("page {} has image, running blank detection", pageIndex);
|
||||||
// Render image and save as temp file
|
// Render image and save as temp file
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
||||||
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
||||||
|
@ -82,34 +83,57 @@ public class BlankPageController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blank) {
|
if (blank) {
|
||||||
logger.info("Skipping, Image was blank for page #" + pageIndex);
|
logger.info("Skipping, Image was blank for page #{}", pageIndex);
|
||||||
|
blankPages.add(page);
|
||||||
} else {
|
} else {
|
||||||
logger.info("page " + pageIndex + " has image which is not blank");
|
logger.info("page {} has image which is not blank", pageIndex);
|
||||||
pagesToKeepIndex.add(pageIndex);
|
nonBlankPages.add(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
}
|
}
|
||||||
// Remove pages not present in pagesToKeepIndex
|
|
||||||
List<Integer> pageIndices =
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
ZipOutputStream zos = new ZipOutputStream(baos);
|
||||||
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
|
||||||
for (Integer i : pageIndices) {
|
String filename =
|
||||||
if (!pagesToKeepIndex.contains(i)) {
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
pages.remove(i);
|
.replaceFirst("[.][^.]+$", "");
|
||||||
}
|
|
||||||
|
if (!nonBlankPages.isEmpty()) {
|
||||||
|
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
|
||||||
|
} else {
|
||||||
|
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
|
||||||
document,
|
createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
}
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_blanksRemoved.pdf");
|
zos.close();
|
||||||
|
|
||||||
|
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||||
|
return WebResponseUtils.boasToWebResponse(
|
||||||
|
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("exception", e);
|
logger.error("exception", e);
|
||||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
} finally {
|
}
|
||||||
if (document != null) document.close();
|
}
|
||||||
|
|
||||||
|
public void createZipEntry(ZipOutputStream zos, List<PDPage> pages, String entryName)
|
||||||
|
throws IOException {
|
||||||
|
try (PDDocument document = new PDDocument()) {
|
||||||
|
|
||||||
|
for (PDPage page : pages) {
|
||||||
|
document.addPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipEntry zipEntry = new ZipEntry(entryName);
|
||||||
|
zos.putNextEntry(zipEntry);
|
||||||
|
document.save(zos);
|
||||||
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ public class CompressController {
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("gs");
|
command.add("gs");
|
||||||
command.add("-sDEVICE=pdfwrite");
|
command.add("-sDEVICE=pdfwrite");
|
||||||
command.add("-dCompatibilityLevel=1.4");
|
command.add("-dCompatibilityLevel=1.5");
|
||||||
|
|
||||||
switch (optimizeLevel) {
|
switch (optimizeLevel) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
|
@ -32,6 +32,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
|
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
@ -76,6 +77,11 @@ public class ExtractImageScansController {
|
||||||
Path tempZipFile = null;
|
Path tempZipFile = null;
|
||||||
List<Path> tempDirs = new ArrayList<>();
|
List<Path> tempDirs = new ArrayList<>();
|
||||||
|
|
||||||
|
if (!CheckProgramInstall.isPythonAvailable()) {
|
||||||
|
throw new IOException("Python is not installed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||||
try {
|
try {
|
||||||
// Check if input file is a PDF
|
// Check if input file is a PDF
|
||||||
if ("pdf".equalsIgnoreCase(extension)) {
|
if ("pdf".equalsIgnoreCase(extension)) {
|
||||||
|
@ -117,7 +123,7 @@ public class ExtractImageScansController {
|
||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"python3",
|
pythonVersion,
|
||||||
"./scripts/split_photos.py",
|
"./scripts/split_photos.py",
|
||||||
images.get(i),
|
images.get(i),
|
||||||
tempDir.toString(),
|
tempDir.toString(),
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Graphics2D;
|
import java.awt.*;
|
||||||
import java.awt.Image;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
@ -47,16 +50,19 @@ public class ExtractImagesController {
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract images from a PDF file",
|
summary = "Extract images from a PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
|
||||||
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
||||||
throws IOException {
|
throws IOException, InterruptedException, ExecutionException {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String format = request.getFormat();
|
||||||
|
|
||||||
System.out.println(
|
System.out.println(
|
||||||
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
|
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
|
// Determine if multithreading should be used based on PDF size or number of pages
|
||||||
|
boolean useMultithreading = shouldUseMultithreading(file, document);
|
||||||
|
|
||||||
// Create ByteArrayOutputStream to write zip file to byte array
|
// Create ByteArrayOutputStream to write zip file to byte array
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
@ -66,71 +72,51 @@ public class ExtractImagesController {
|
||||||
// Set compression level
|
// Set compression level
|
||||||
zos.setLevel(Deflater.BEST_COMPRESSION);
|
zos.setLevel(Deflater.BEST_COMPRESSION);
|
||||||
|
|
||||||
int imageIndex = 1;
|
|
||||||
String filename =
|
String filename =
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "");
|
||||||
int pageNum = 0;
|
|
||||||
Set<Integer> processedImages = new HashSet<>();
|
Set<Integer> processedImages = new HashSet<>();
|
||||||
|
|
||||||
|
if (useMultithreading) {
|
||||||
|
// Executor service to handle multithreading
|
||||||
|
ExecutorService executor =
|
||||||
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
|
Set<Future<Void>> futures = new HashSet<>();
|
||||||
|
|
||||||
// Iterate over each page
|
// Iterate over each page
|
||||||
for (PDPage page : document.getPages()) {
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
++pageNum;
|
PDPage page = document.getPage(pgNum);
|
||||||
// Extract images from page
|
int pageNum = document.getPages().indexOf(page) + 1;
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
// Submit a task for processing each page
|
||||||
if (page.getResources().isImageXObject(name)) {
|
Future<Void> future =
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
executor.submit(
|
||||||
int imageHash = image.hashCode();
|
() -> {
|
||||||
if (processedImages.contains(imageHash)) {
|
extractImagesFromPage(
|
||||||
continue; // Skip already processed images
|
page, format, filename, pageNum, processedImages, zos);
|
||||||
}
|
return null;
|
||||||
processedImages.add(imageHash);
|
});
|
||||||
|
|
||||||
// Convert image to desired format
|
futures.add(future);
|
||||||
RenderedImage renderedImage = image.getImage();
|
|
||||||
BufferedImage bufferedImage = null;
|
|
||||||
if ("png".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_INT_ARGB);
|
|
||||||
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_INT_RGB);
|
|
||||||
} else if ("gif".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_BYTE_INDEXED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write image to zip file
|
// Wait for all tasks to complete
|
||||||
String imageName =
|
for (Future<Void> future : futures) {
|
||||||
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
future.get();
|
||||||
ZipEntry zipEntry = new ZipEntry(imageName);
|
|
||||||
zos.putNextEntry(zipEntry);
|
|
||||||
|
|
||||||
Graphics2D g = bufferedImage.createGraphics();
|
|
||||||
g.drawImage((Image) renderedImage, 0, 0, null);
|
|
||||||
g.dispose();
|
|
||||||
// Write image bytes to zip file
|
|
||||||
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(bufferedImage, format, imageBaos);
|
|
||||||
zos.write(imageBaos.toByteArray());
|
|
||||||
|
|
||||||
zos.closeEntry();
|
|
||||||
imageIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close executor service
|
||||||
|
executor.shutdown();
|
||||||
|
} else {
|
||||||
|
// Single-threaded extraction
|
||||||
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
|
PDPage page = document.getPage(pgNum);
|
||||||
|
extractImagesFromPage(page, format, filename, pgNum + 1, processedImages, zos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close ZipOutputStream and PDDocument
|
// Close PDDocument and ZipOutputStream
|
||||||
zos.close();
|
|
||||||
document.close();
|
document.close();
|
||||||
|
zos.close();
|
||||||
|
|
||||||
// Create ByteArrayResource from byte array
|
// Create ByteArrayResource from byte array
|
||||||
byte[] zipContents = baos.toByteArray();
|
byte[] zipContents = baos.toByteArray();
|
||||||
|
@ -138,4 +124,72 @@ public class ExtractImagesController {
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.boasToWebResponse(
|
||||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
|
||||||
|
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
|
||||||
|
long fileSizeInMB = file.getSize() / (1024 * 1024);
|
||||||
|
int numberOfPages = document.getPages().getCount();
|
||||||
|
return fileSizeInMB > 10 || numberOfPages > 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractImagesFromPage(
|
||||||
|
PDPage page,
|
||||||
|
String format,
|
||||||
|
String filename,
|
||||||
|
int pageNum,
|
||||||
|
Set<Integer> processedImages,
|
||||||
|
ZipOutputStream zos)
|
||||||
|
throws IOException {
|
||||||
|
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (COSName name : page.getResources().getXObjectNames()) {
|
||||||
|
if (page.getResources().isImageXObject(name)) {
|
||||||
|
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||||
|
int imageHash = image.hashCode();
|
||||||
|
synchronized (processedImages) {
|
||||||
|
if (processedImages.contains(imageHash)) {
|
||||||
|
continue; // Skip already processed images
|
||||||
|
}
|
||||||
|
processedImages.add(imageHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderedImage renderedImage = image.getImage();
|
||||||
|
|
||||||
|
// Convert to standard RGB colorspace if needed
|
||||||
|
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
||||||
|
|
||||||
|
// Write image to zip file
|
||||||
|
String imageName = filename + "_" + imageHash + " (Page " + pageNum + ")." + format;
|
||||||
|
synchronized (zos) {
|
||||||
|
zos.putNextEntry(new ZipEntry(imageName));
|
||||||
|
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(bufferedImage, format, imageBaos);
|
||||||
|
zos.write(imageBaos.toByteArray());
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
|
||||||
|
int width = renderedImage.getWidth();
|
||||||
|
int height = renderedImage.getHeight();
|
||||||
|
BufferedImage rgbImage;
|
||||||
|
|
||||||
|
if ("png".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
} else if ("gif".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
|
||||||
|
} else {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics2D g = rgbImage.createGraphics();
|
||||||
|
g.drawImage((Image) renderedImage, 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
return rgbImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,7 @@ 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;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
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;
|
||||||
|
@ -27,6 +26,7 @@ import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
@ -37,10 +37,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OCRController {
|
public class OCRController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tessdata";
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class ApiDocService {
|
||||||
|
|
||||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||||
|
|
||||||
public List 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"));
|
||||||
outputToFileTypes.put(
|
outputToFileTypes.put(
|
||||||
|
|
|
@ -1,21 +1,14 @@
|
||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
@ -32,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import stirling.software.SPDF.model.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
import stirling.software.SPDF.pdf.TextFinder;
|
import stirling.software.SPDF.pdf.TextFinder;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -81,22 +75,9 @@ public class RedactController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convertPDFToImage) {
|
if (convertPDFToImage) {
|
||||||
PDDocument imageDocument = new PDDocument();
|
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
|
||||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
|
||||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
|
||||||
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
|
|
||||||
imageDocument.addPage(newPage);
|
|
||||||
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
|
||||||
PDPageContentStream contentStream =
|
|
||||||
new PDPageContentStream(
|
|
||||||
imageDocument, newPage, AppendMode.APPEND, true, true);
|
|
||||||
contentStream.drawImage(pdImage, 0, 0);
|
|
||||||
contentStream.close();
|
|
||||||
}
|
|
||||||
document.close();
|
document.close();
|
||||||
document = imageDocument;
|
document = convertedPdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
|
@ -36,6 +36,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@ -60,6 +61,7 @@ public class WatermarkController {
|
||||||
float opacity = request.getOpacity();
|
float opacity = request.getOpacity();
|
||||||
int widthSpacer = request.getWidthSpacer();
|
int widthSpacer = request.getWidthSpacer();
|
||||||
int heightSpacer = request.getHeightSpacer();
|
int heightSpacer = request.getHeightSpacer();
|
||||||
|
boolean convertPdfToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
|
@ -104,6 +106,12 @@ public class WatermarkController {
|
||||||
contentStream.close();
|
contentStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (convertPdfToImage) {
|
||||||
|
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
||||||
|
document.close();
|
||||||
|
document = convertedPdf;
|
||||||
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import java.util.Iterator;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.util.List;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import java.util.Map;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Optional;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
@ -16,34 +13,35 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
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.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import stirling.software.SPDF.model.*;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.Authority;
|
|
||||||
import stirling.software.SPDF.model.Role;
|
|
||||||
import stirling.software.SPDF.model.User;
|
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
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.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Slf4j
|
||||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
|
@Autowired SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
|
||||||
|
// If the user is already authenticated, redirect them to the home page.
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
@ -137,6 +135,13 @@ public class AccountWebController {
|
||||||
break;
|
break;
|
||||||
case "invalid_id_token":
|
case "invalid_id_token":
|
||||||
erroroauth = "login.oauth2InvalidIdToken";
|
erroroauth = "login.oauth2InvalidIdToken";
|
||||||
|
break;
|
||||||
|
case "oauth2_admin_blocked_user":
|
||||||
|
erroroauth = "login.oauth2AdminBlockedUser";
|
||||||
|
break;
|
||||||
|
case "userIsDisabled":
|
||||||
|
erroroauth = "login.userIsDisabled";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -155,9 +160,6 @@ public class AccountWebController {
|
||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@GetMapping("/addUsers")
|
@GetMapping("/addUsers")
|
||||||
public String showAddUserForm(
|
public String showAddUserForm(
|
||||||
|
@ -166,6 +168,13 @@ public class AccountWebController {
|
||||||
Iterator<User> iterator = allUsers.iterator();
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||||
|
|
||||||
|
// Map to store session information and user activity status
|
||||||
|
Map<String, Boolean> userSessions = new HashMap<>();
|
||||||
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
|
||||||
|
int activeUsers = 0;
|
||||||
|
int disabledUsers = 0;
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
User user = iterator.next();
|
User user = iterator.next();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
@ -176,8 +185,72 @@ public class AccountWebController {
|
||||||
break; // Break out of the inner loop once the user is removed
|
break; // Break out of the inner loop once the user is removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the user's session status and last request time
|
||||||
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
|
boolean hasActiveSession = false;
|
||||||
|
Date lastRequest = null;
|
||||||
|
|
||||||
|
Optional<SessionEntity> latestSession =
|
||||||
|
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||||
|
if (latestSession.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = latestSession.get();
|
||||||
|
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
// Calculate session expiration and update session status accordingly
|
||||||
|
Instant expirationTime =
|
||||||
|
lastAccessedTime
|
||||||
|
.toInstant()
|
||||||
|
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
|
if (now.isAfter(expirationTime)) {
|
||||||
|
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||||
|
hasActiveSession = false;
|
||||||
|
} else {
|
||||||
|
hasActiveSession = !sessionEntity.isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRequest = sessionEntity.getLastRequest();
|
||||||
|
} else {
|
||||||
|
hasActiveSession = false;
|
||||||
|
lastRequest = new Date(0); // No session, set default last request time
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessions.put(user.getUsername(), hasActiveSession);
|
||||||
|
userLastRequest.put(user.getUsername(), lastRequest);
|
||||||
|
|
||||||
|
if (hasActiveSession) {
|
||||||
|
activeUsers++;
|
||||||
|
}
|
||||||
|
if (!user.isEnabled()) {
|
||||||
|
disabledUsers++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort users by active status and last request date
|
||||||
|
List<User> sortedUsers =
|
||||||
|
allUsers.stream()
|
||||||
|
.sorted(
|
||||||
|
(u1, u2) -> {
|
||||||
|
boolean u1Active = userSessions.get(u1.getUsername());
|
||||||
|
boolean u2Active = userSessions.get(u2.getUsername());
|
||||||
|
|
||||||
|
if (u1Active && !u2Active) {
|
||||||
|
return -1;
|
||||||
|
} else if (!u1Active && u2Active) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
Date u1LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u1.getUsername(), new Date(0));
|
||||||
|
Date u2LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u2.getUsername(), new Date(0));
|
||||||
|
return u2LastRequest.compareTo(u1LastRequest);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
|
|
||||||
|
@ -203,6 +276,9 @@ public class AccountWebController {
|
||||||
case "invalidUsername":
|
case "invalidUsername":
|
||||||
addMessage = "invalidUsernameMessage";
|
addMessage = "invalidUsernameMessage";
|
||||||
break;
|
break;
|
||||||
|
case "invalidPassword":
|
||||||
|
addMessage = "invalidPasswordMessage";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -218,16 +294,24 @@ public class AccountWebController {
|
||||||
case "downgradeCurrentUser":
|
case "downgradeCurrentUser":
|
||||||
changeMessage = "downgradeCurrentUserMessage";
|
changeMessage = "downgradeCurrentUserMessage";
|
||||||
break;
|
break;
|
||||||
|
case "disabledCurrentUser":
|
||||||
|
changeMessage = "disabledCurrentUserMessage";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
changeMessage = messageType;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
model.addAttribute("changeMessage", changeMessage);
|
model.addAttribute("changeMessage", changeMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("users", allUsers);
|
model.addAttribute("users", sortedUsers);
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
model.addAttribute("roleDetails", roleDetails);
|
model.addAttribute("roleDetails", roleDetails);
|
||||||
|
model.addAttribute("userSessions", userSessions);
|
||||||
|
model.addAttribute("userLastRequest", userLastRequest);
|
||||||
|
model.addAttribute("totalUsers", allUsers.size());
|
||||||
|
model.addAttribute("activeUsers", activeUsers);
|
||||||
|
model.addAttribute("disabledUsers", disabledUsers);
|
||||||
return "addUsers";
|
return "addUsers";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +349,7 @@ public class AccountWebController {
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user =
|
Optional<User> user =
|
||||||
userRepository.findByUsernameIgnoreCase(
|
userRepository.findByUsernameIgnoreCaseWithSettings(
|
||||||
username); // Assuming findByUsername method exists
|
username); // Assuming findByUsername method exists
|
||||||
if (!user.isPresent()) {
|
if (!user.isPresent()) {
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
|
@ -278,7 +362,7 @@ public class AccountWebController {
|
||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
// Handle JSON conversion error
|
||||||
logger.error("exception", e);
|
log.error("exception", e);
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ import org.springframework.web.servlet.ModelAndView;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConverterWebController {
|
public class ConverterWebController {
|
||||||
|
@ -21,14 +23,6 @@ public class ConverterWebController {
|
||||||
return "convert/book-to-pdf";
|
return "convert/book-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
|
||||||
@GetMapping("/pdf-to-book")
|
|
||||||
@Hidden
|
|
||||||
public String convertPdfToBookForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pdf-to-book");
|
|
||||||
return "convert/pdf-to-book";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/img-to-pdf")
|
@GetMapping("/img-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertImgToPdfForm(Model model) {
|
public String convertImgToPdfForm(Model model) {
|
||||||
|
@ -57,13 +51,6 @@ public class ConverterWebController {
|
||||||
return "convert/url-to-pdf";
|
return "convert/url-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-img")
|
|
||||||
@Hidden
|
|
||||||
public String pdfToimgForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pdf-to-img");
|
|
||||||
return "convert/pdf-to-img";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/file-to-pdf")
|
@GetMapping("/file-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertToPdfForm(Model model) {
|
public String convertToPdfForm(Model model) {
|
||||||
|
@ -73,6 +60,23 @@ public class ConverterWebController {
|
||||||
|
|
||||||
// PDF TO......
|
// PDF TO......
|
||||||
|
|
||||||
|
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
||||||
|
@GetMapping("/pdf-to-book")
|
||||||
|
@Hidden
|
||||||
|
public String convertPdfToBookForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "pdf-to-book");
|
||||||
|
return "convert/pdf-to-book";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pdf-to-img")
|
||||||
|
@Hidden
|
||||||
|
public String pdfToimgForm(Model model) {
|
||||||
|
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||||
|
model.addAttribute("isPython", isPython);
|
||||||
|
model.addAttribute("currentPage", "pdf-to-img");
|
||||||
|
return "convert/pdf-to-img";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-html")
|
@GetMapping("/pdf-to-html")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView pdfToHTML() {
|
public ModelAndView pdfToHTML() {
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
||||||
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@Tag(name = "Database Management", description = "Database management and security APIs")
|
||||||
|
public class DatabaseWebController {
|
||||||
|
|
||||||
|
@Autowired private DatabaseBackupHelper databaseBackupHelper;
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@GetMapping("/database")
|
||||||
|
public String database(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
String error = request.getParameter("error");
|
||||||
|
String confirmed = request.getParameter("infoMessage");
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
model.addAttribute("error", error);
|
||||||
|
} else if (confirmed != null) {
|
||||||
|
model.addAttribute("infoMessage", confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
||||||
|
model.addAttribute("systemUpdate", backupList);
|
||||||
|
|
||||||
|
return "database";
|
||||||
|
}
|
||||||
|
}
|
|
@ -310,4 +310,11 @@ public class GeneralWebController {
|
||||||
model.addAttribute("currentPage", "auto-split-pdf");
|
model.addAttribute("currentPage", "auto-split-pdf");
|
||||||
return "auto-split-pdf";
|
return "auto-split-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/remove-image-pdf")
|
||||||
|
@Hidden
|
||||||
|
public String removeImagePdfForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "remove-image-pdf");
|
||||||
|
return "remove-image-pdf";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
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.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
@ -14,10 +15,15 @@ import org.springframework.web.servlet.ModelAndView;
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OtherWebController {
|
public class OtherWebController {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@GetMapping("/compress-pdf")
|
@GetMapping("/compress-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String compressPdfForm(Model model) {
|
public String compressPdfForm(Model model) {
|
||||||
|
@ -29,6 +35,8 @@ public class OtherWebController {
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView extractImageScansForm() {
|
public ModelAndView extractImageScansForm() {
|
||||||
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
||||||
|
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
||||||
|
modelAndView.addObject("isPython", isPython);
|
||||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +105,7 @@ public class OtherWebController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tessdata";
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
|
@ -241,6 +241,7 @@ public class ApplicationProperties {
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private Boolean autoCreateUser = false;
|
private Boolean autoCreateUser = false;
|
||||||
|
private Boolean blockRegistration = false;
|
||||||
private String useAsUsername;
|
private String useAsUsername;
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
private String provider;
|
private String provider;
|
||||||
|
@ -286,6 +287,14 @@ public class ApplicationProperties {
|
||||||
this.autoCreateUser = autoCreateUser;
|
this.autoCreateUser = autoCreateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getBlockRegistration() {
|
||||||
|
return blockRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockRegistration(Boolean blockRegistration) {
|
||||||
|
this.blockRegistration = blockRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
public String getUseAsUsername() {
|
public String getUseAsUsername() {
|
||||||
return useAsUsername;
|
return useAsUsername;
|
||||||
}
|
}
|
||||||
|
@ -356,10 +365,14 @@ public class ApplicationProperties {
|
||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
+ ", autoCreateUser="
|
+ ", autoCreateUser="
|
||||||
+ autoCreateUser
|
+ autoCreateUser
|
||||||
|
+ ", blockRegistration="
|
||||||
|
+ blockRegistration
|
||||||
+ ", useAsUsername="
|
+ ", useAsUsername="
|
||||||
+ useAsUsername
|
+ useAsUsername
|
||||||
+ ", provider="
|
+ ", provider="
|
||||||
+ provider
|
+ provider
|
||||||
|
+ ", client="
|
||||||
|
+ client
|
||||||
+ ", scopes="
|
+ ", scopes="
|
||||||
+ scopes
|
+ scopes
|
||||||
+ "]";
|
+ "]";
|
||||||
|
@ -429,6 +442,15 @@ public class ApplicationProperties {
|
||||||
private boolean showUpdate;
|
private boolean showUpdate;
|
||||||
private Boolean showUpdateOnlyAdmin;
|
private Boolean showUpdateOnlyAdmin;
|
||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
|
private String tessdataDir;
|
||||||
|
|
||||||
|
public String getTessdataDir() {
|
||||||
|
return tessdataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTessdataDir(String tessdataDir) {
|
||||||
|
this.tessdataDir = tessdataDir;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isCustomHTMLFiles() {
|
public boolean isCustomHTMLFiles() {
|
||||||
return customHTMLFiles;
|
return customHTMLFiles;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
@ -11,7 +13,9 @@ import jakarta.persistence.Table;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "authorities")
|
@Table(name = "authorities")
|
||||||
public class Authority {
|
public class Authority implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
public Authority() {}
|
public Authority() {}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@Table(name = "sessions")
|
||||||
|
public class SessionEntity implements Serializable {
|
||||||
|
@Id private String sessionId;
|
||||||
|
|
||||||
|
@Lob private String principalName;
|
||||||
|
|
||||||
|
private Date lastRequest;
|
||||||
|
|
||||||
|
private boolean expired;
|
||||||
|
}
|
|
@ -1,29 +1,19 @@
|
||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import jakarta.persistence.CascadeType;
|
|
||||||
import jakarta.persistence.CollectionTable;
|
|
||||||
import jakarta.persistence.Column;
|
|
||||||
import jakarta.persistence.ElementCollection;
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.FetchType;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
|
||||||
import jakarta.persistence.GenerationType;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.JoinColumn;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
import jakarta.persistence.MapKeyColumn;
|
|
||||||
import jakarta.persistence.OneToMany;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
public class User {
|
public class User implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class ConvertToImageRequest extends PDFFile {
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The output image format",
|
description = "The output image format",
|
||||||
allowableValues = {"png", "jpeg", "jpg", "gif"})
|
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"})
|
||||||
private String imageFormat;
|
private String imageFormat;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
|
|
|
@ -9,6 +9,8 @@ import lombok.EqualsAndHashCode;
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
public class UrlToPdfRequest {
|
public class UrlToPdfRequest {
|
||||||
|
|
||||||
@Schema(description = "The input URL to be converted to a PDF file", required = true)
|
@Schema(
|
||||||
|
description = "The input URL to be converted to a PDF file",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String urlInput;
|
private String urlInput;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class ContainsTextRequest extends PDFWithPageNums {
|
public class ContainsTextRequest extends PDFWithPageNums {
|
||||||
|
|
||||||
@Schema(description = "The text to check for", required = true)
|
@Schema(description = "The text to check for", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String text;
|
private String text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class FileSizeRequest extends PDFComparison {
|
public class FileSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "File Size", required = true)
|
@Schema(description = "File Size", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String fileSize;
|
private String fileSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PageRotationRequest extends PDFComparison {
|
public class PageRotationRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "Rotation in degrees", required = true)
|
@Schema(description = "Rotation in degrees", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private int rotation;
|
private int rotation;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PageSizeRequest extends PDFComparison {
|
public class PageSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "Standard Page Size", required = true)
|
@Schema(description = "Standard Page Size", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String standardPageSize;
|
private String standardPageSize;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,13 +20,13 @@ public class OverlayPdfsRequest extends PDFFile {
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts",
|
"The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts",
|
||||||
required = true)
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String overlayMode;
|
private String overlayMode;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.",
|
"An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.",
|
||||||
required = false)
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
||||||
private int[] counts;
|
private int[] counts;
|
||||||
|
|
||||||
@Schema(description = "Overlay position 0 is Foregound, 1 is Background")
|
@Schema(description = "Overlay position 0 is Foregound, 1 is Background")
|
||||||
|
|
|
@ -13,14 +13,14 @@ public class SplitPdfBySizeOrCountRequest extends PDFFile {
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Determines the type of split: 0 for size, 1 for page count, 2 for document count",
|
"Determines the type of split: 0 for size, 1 for page count, 2 for document count",
|
||||||
required = false,
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
defaultValue = "0")
|
defaultValue = "0")
|
||||||
private int splitType;
|
private int splitType;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')",
|
"Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')",
|
||||||
required = false,
|
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
||||||
defaultValue = "10MB")
|
defaultValue = "10MB")
|
||||||
private String splitValue;
|
private String splitValue;
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue