Compare commits
212 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
79927416e5 | ||
|
|
f95ee31bbd | ||
|
|
40042c37f2 | ||
|
|
1c90b65bca | ||
|
|
2971425544 | ||
|
|
a9ee698432 | ||
|
|
15b5d51957 | ||
|
|
92893b8d4c | ||
|
|
3a6969cad0 | ||
|
|
88e8663d44 | ||
|
|
a2d7490d45 | ||
|
|
c363a1c4e0 | ||
|
|
9e84836cfe | ||
|
|
c8a2789caf | ||
|
|
202b996c2b | ||
|
|
e766a5f583 | ||
|
|
c7d18939fc | ||
|
|
34e198d8e6 | ||
|
|
f6a31e6ed0 | ||
|
|
7768dc9fd9 | ||
|
|
b739d3847e | ||
|
|
a3e5cb51b0 | ||
|
|
ab7a41d155 | ||
|
|
3576c32c52 | ||
|
|
f43fe15193 | ||
|
|
234ae17dc8 | ||
|
|
7ee14ac794 | ||
|
|
ba604bda3e | ||
|
|
036c10fc27 | ||
|
|
baa9410242 | ||
|
|
ab2fe5ef81 | ||
|
|
a926289fb0 | ||
|
|
b711be3061 | ||
|
|
f07ba9192b | ||
|
|
dc2f891632 | ||
|
|
fe9c5a7351 | ||
|
|
d575ba8f9a | ||
|
|
accab3b5bf | ||
|
|
8cbb4367ab | ||
|
|
3ede204918 | ||
|
|
32030e8d85 | ||
|
|
b7d55a3f78 | ||
|
|
699545ddbe | ||
|
|
54b3f17bf9 | ||
|
|
f2015cecbd | ||
|
|
f60a8d87d2 | ||
|
|
eccebd265f | ||
|
|
889c9a114b | ||
|
|
9fd561ecf6 | ||
|
|
1e72960c5f | ||
|
|
5a50c54f29 | ||
|
|
446bc68768 | ||
|
|
76660eb791 | ||
|
|
e0ce3c90de | ||
|
|
aaf94fa981 | ||
|
|
d1aa56266e | ||
|
|
790d4f053d | ||
|
|
e5b25ac8a5 | ||
|
|
b365155e62 | ||
|
|
f4f80a54a8 | ||
|
|
681a8d3443 | ||
|
|
7543f49ba4 | ||
|
|
2e11b632dd | ||
|
|
63bdc0d59e | ||
|
|
56fdf1f3a1 | ||
|
|
a6069c0f9e | ||
|
|
327a44d487 | ||
|
|
7a15930453 | ||
|
|
517e54517c | ||
|
|
ef59ea6fe4 | ||
|
|
10472a6467 | ||
|
|
9a9429c15c | ||
|
|
b17912d607 | ||
|
|
ff315d9d96 | ||
|
|
67f72ad17b | ||
|
|
8f55c38391 | ||
|
|
8245d77c84 | ||
|
|
f5628f16d9 | ||
|
|
47907daea0 | ||
|
|
664253532e | ||
|
|
6108b38098 | ||
|
|
a634c63176 | ||
|
|
109ed6a719 | ||
|
|
ac9312bd7e | ||
|
|
a743493cd3 | ||
|
|
c35355f01a | ||
|
|
ed910da288 | ||
|
|
fc0878704d | ||
|
|
d5c71c8425 | ||
|
|
afbe8f7abe | ||
|
|
3a97ff0e5e | ||
|
|
4f5b19edfb | ||
|
|
88338b4dbc | ||
|
|
fc249661e2 | ||
|
|
805848e627 | ||
|
|
fdae1c9756 | ||
|
|
32b3cfca41 | ||
|
|
9147d364bc | ||
|
|
6606850e4a | ||
|
|
5277cf2b59 | ||
|
|
52e9689431 | ||
|
|
caa5525ddd |
2
.gitattributes
vendored
@@ -3,6 +3,8 @@
|
|||||||
# Ignore all JavaScript files in a directory
|
# Ignore all JavaScript files in a directory
|
||||||
src/main/resources/static/pdfjs/* linguist-vendored
|
src/main/resources/static/pdfjs/* linguist-vendored
|
||||||
src/main/resources/static/pdfjs/** linguist-vendored
|
src/main/resources/static/pdfjs/** linguist-vendored
|
||||||
|
src/main/resources/static/pdfjs-legacy/* linguist-vendored
|
||||||
|
src/main/resources/static/pdfjs-legacy/** linguist-vendored
|
||||||
src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
src/main/resources/static/css/bootstrap-icons.css linguist-vendored
|
||||||
src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
src/main/resources/static/css/bootstrap.min.css linguist-vendored
|
||||||
src/main/resources/static/css/fonts/* linguist-vendored
|
src/main/resources/static/css/fonts/* linguist-vendored
|
||||||
|
|||||||
116
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: File a bug report.
|
||||||
|
title: "[Bug]: "
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Bug Report
|
||||||
|
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
|
||||||
|
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: The Problem
|
||||||
|
description: |
|
||||||
|
Describe the issue you are experiencing here. Tell us what you were trying to do and what happened.
|
||||||
|
|
||||||
|
Provide a clear and concise description of what the problem is.
|
||||||
|
placeholder: Provide a detailed description of the issue.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Version of Stirling-PDF
|
||||||
|
placeholder: e.g., 0.0.2
|
||||||
|
description: What version of Stirling-PDF has the issue?
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: last-working-version
|
||||||
|
attributes:
|
||||||
|
label: Last Working Version of Stirling-PDF
|
||||||
|
placeholder: e.g., 0.0.1
|
||||||
|
description: |
|
||||||
|
If known, please provide the last version where the issue did not occur. Otherwise, leave blank.
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: url
|
||||||
|
attributes:
|
||||||
|
label: Page Where the Problem Occurred
|
||||||
|
placeholder: e.g., http://localhost:8080/pdf/pipeline
|
||||||
|
description: |
|
||||||
|
If applicable, provide the URL where the issue occurred. Otherwise, leave blank.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: docker
|
||||||
|
attributes:
|
||||||
|
label: Docker Configuration
|
||||||
|
description: |
|
||||||
|
Enter your Docker configuration here if it is relevant to the error. Remove any personal data. Otherwise, leave the field blank.
|
||||||
|
render: txt
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Relevant Log Output
|
||||||
|
description: |
|
||||||
|
Provide any log output that might help us diagnose the issue, such as error messages or stack traces.
|
||||||
|
render: txt
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Additional Information
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: |
|
||||||
|
If you have any additional information that might help us understand and resolve the issue, provide it here.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Browser Information
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: Browsers Affected
|
||||||
|
description: |
|
||||||
|
If applicable, select the browsers where you are experiencing the issue. Otherwise, leave blank.
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Firefox
|
||||||
|
- Chrome
|
||||||
|
- Safari
|
||||||
|
- Microsoft Edge
|
||||||
|
- Other
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: terms
|
||||||
|
attributes:
|
||||||
|
label: No Duplicate of the Issue
|
||||||
|
description: |
|
||||||
|
Please confirm that you have searched for similar issues and none of them match your problem.
|
||||||
|
options:
|
||||||
|
- label: I have verified that there are no existing issues raised related to my problem.
|
||||||
|
required: true
|
||||||
78
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Submit a new feature request.
|
||||||
|
title: "[Feature Request]: "
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Feature Request
|
||||||
|
|
||||||
|
Thank you for taking the time to suggest a new feature!
|
||||||
|
|
||||||
|
This form is for proposing features or enhancements. Please fill out the following sections to help us understand your idea or suggestion.
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: feature-description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: Feature Description
|
||||||
|
description: |
|
||||||
|
Describe the feature you would like to see. Tell us what the feature should do and the problem it would solve.
|
||||||
|
|
||||||
|
Provide a clear and concise description of what you want to happen.
|
||||||
|
placeholder: Provide a detailed description of the desired feature.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: motivation
|
||||||
|
attributes:
|
||||||
|
label: Why is this feature valuable?
|
||||||
|
description: |
|
||||||
|
Explain why this feature is valuable to you or others. How would it improve the tool or process?
|
||||||
|
|
||||||
|
Describe any relevant scenarios that would benefit from this feature.
|
||||||
|
placeholder: Describe why this feature is important.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Possible Implementation
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: implementation
|
||||||
|
attributes:
|
||||||
|
label: Suggested Implementation
|
||||||
|
description: |
|
||||||
|
If you have ideas about how this feature could be implemented, describe them here.
|
||||||
|
|
||||||
|
This section is optional but can be helpful to guide initial discussions.
|
||||||
|
placeholder: Describe how this feature might be implemented.
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## Additional Information
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-info
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: |
|
||||||
|
If you have any additional information, comments, or resources you think would support or be relevant to your feature request, include them here.
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: search-confirmation
|
||||||
|
attributes:
|
||||||
|
label: No Duplicate of the Feature
|
||||||
|
description: |
|
||||||
|
Please confirm that you have searched for similar features in our repository and found none that match your request.
|
||||||
|
options:
|
||||||
|
- label: I have verified that there are no existing features requests similar to my request.
|
||||||
|
required: true
|
||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💬 Discord Server
|
||||||
|
url: https://discord.gg/Cn8pWhQRxZ
|
||||||
|
about: You can join our Discord server for real time discussion and support
|
||||||
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:
|
||||||
|
|||||||
53
.github/labeler-config.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
Translation:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
||||||
|
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||||
|
|
||||||
|
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
@@ -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"
|
||||||
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
@@ -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
@@ -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@v3
|
||||||
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@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
|
||||||
|
|||||||
16
.github/workflows/licenses-update.yml
vendored
@@ -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:
|
||||||
@@ -58,5 +59,20 @@ jobs:
|
|||||||
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
@@ -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
|
||||||
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
|
||||||
16
.github/workflows/sync_files.yml
vendored
@@ -7,7 +7,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "build.gradle"
|
- "build.gradle"
|
||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
- "scripts/translation_status.toml"
|
- "scripts/ignore_translation.toml"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -17,9 +17,9 @@ jobs:
|
|||||||
sync-versions:
|
sync-versions:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
|
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6.0.1
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
@@ -51,12 +51,13 @@ 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:
|
||||||
- uses: actions/checkout@v4.1.1
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5.1.0
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -73,7 +74,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":memo: Sync README
|
git diff --staged --quiet || git commit -m ":memo: Sync README
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6.0.1
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
@@ -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
@@ -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
|
|
||||||
47
.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
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ repos:
|
|||||||
args:
|
args:
|
||||||
- --fix
|
- --fix
|
||||||
- --line-length=127
|
- --line-length=127
|
||||||
files: ^((.github/scripts)/.+)?[^/]+\.py$
|
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
||||||
|
exclude: (split_photos.py)
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
files: ^((.github/scripts)/.+)?[^/]+\.py$
|
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
|
||||||
|
exclude: (split_photos.py)
|
||||||
- repo: https://github.com/codespell-project/codespell
|
- repo: https://github.com/codespell-project/codespell
|
||||||
rev: v2.2.6
|
rev: v2.2.6
|
||||||
hooks:
|
hooks:
|
||||||
@@ -33,5 +35,5 @@ repos:
|
|||||||
# args: ["--replace_with= "]
|
# args: ["--replace_with= "]
|
||||||
entry: python .github/scripts/check_tabulator.py
|
entry: python .github/scripts/check_tabulator.py
|
||||||
language: python
|
language: python
|
||||||
exclude: ^src/main/resources/static/pdfjs/
|
exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
|
||||||
files: ^.*(\.html|\.css|\.js)$
|
files: ^.*(\.html|\.css|\.js)$
|
||||||
|
|||||||
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
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -45,8 +45,8 @@ 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 && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build the application
|
# Build the application
|
||||||
FROM gradle:7.6-jdk17 AS build
|
FROM gradle:8.7-jdk17 AS build
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -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,8 +61,8 @@ 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 && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|||||||
169
README.md
@@ -9,6 +9,7 @@
|
|||||||
[](https://github.com/sponsors/Frooodle)
|
[](https://github.com/sponsors/Frooodle)
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
|
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216®ister=true)
|
||||||
|
|
||||||
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
||||||
|
|
||||||
@@ -21,10 +22,11 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Dark mode support.
|
- Dark mode support.
|
||||||
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example)
|
- Custom download options
|
||||||
- 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**
|
||||||
|
|
||||||
@@ -82,7 +84,8 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Get all information on a PDF to view or export as JSON.
|
- Get all information on a PDF to view or export as JSON.
|
||||||
|
|
||||||
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo
|
|
||||||
|
Demo of the app is available [here](https://stirlingpdf.io).
|
||||||
|
|
||||||
## Technologies used
|
## Technologies used
|
||||||
|
|
||||||
@@ -105,33 +108,36 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGui
|
|||||||
|
|
||||||
https://hub.docker.com/r/frooodle/s-pdf
|
https://hub.docker.com/r/frooodle/s-pdf
|
||||||
|
|
||||||
Stirling PDF has 2 different versions, a Full version and ultra-Lite version. Depending on the types of features you use you may want a smaller image to save on space.
|
Stirling PDF has 3 different versions, a Full version and ultra-Lite version as well as a 'Fat' version. Depending on the types of features you use you may want a smaller image to save on space.
|
||||||
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
||||||
For people that don't mind about space optimization just use the latest tag.
|
For people that don't mind about space optimization just use the latest tag.
|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
Docker Run
|
Please note in below examples you may need to change the volume paths as needed, current examples install them to the current working directory
|
||||||
|
eg ``./extraConfigs:/configs`` to ``/opt/stirlingpdf/extraConfigs:/configs``
|
||||||
|
|
||||||
|
### Docker Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /location/of/trainingData:/usr/share/tessdata \
|
-v ./trainingData:/usr/share/tessdata \
|
||||||
-v /location/of/extraConfigs:/configs \
|
-v ./extraConfigs:/configs \
|
||||||
-v /location/of/logs:/logs \
|
-v ./logs:/logs \
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
-e LANGS=en_GB \
|
-e LANGS=en_GB \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
frooodle/s-pdf:latest
|
||||||
|
|
||||||
|
|
||||||
Can also add these for customisation but are not required
|
Can also add these for customisation but are not required
|
||||||
|
|
||||||
-v /location/of/customFiles:/customFiles \
|
-v /location/of/customFiles:/customFiles \
|
||||||
```
|
```
|
||||||
|
|
||||||
Docker Compose
|
### Docker Compose
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.3'
|
version: '3.3'
|
||||||
@@ -141,10 +147,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
- /location/of/trainingData:/usr/share/tessdata #Required for extra OCR languages
|
- ./trainingData:/usr/share/tessdata #Required for extra OCR languages
|
||||||
- /location/of/extraConfigs:/configs
|
- ./extraConfigs:/configs
|
||||||
# - /location/of/customFiles:/customFiles/
|
# - ./customFiles:/customFiles/
|
||||||
# - /location/of/logs:/logs/
|
# - ./logs:/logs/
|
||||||
environment:
|
environment:
|
||||||
- DOCKER_ENABLE_SECURITY=false
|
- DOCKER_ENABLE_SECURITY=false
|
||||||
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
@@ -159,41 +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) |  |
|
||||||
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
|
| Catalan (Català) (ca_CA) |  |
|
||||||
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
|
| Danish (Dansk) (da_DK) |  |
|
||||||
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
|
||||||
| German (Deutsch) (de_DE) |  |
|
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
@@ -205,7 +216,7 @@ Stirling PDF allows easy customization of the app.
|
|||||||
Includes things like
|
Includes things like
|
||||||
|
|
||||||
- Custom application name
|
- Custom application name
|
||||||
- Custom slogans, icons, HTML, images CSS etc (via file overrides)
|
- Custom slogans, icons, HTML, images CSS etc (via file overrides)
|
||||||
|
|
||||||
There are two options for this, either using the generated settings file ``settings.yml``
|
There are two options for this, either using the generated settings file ``settings.yml``
|
||||||
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
||||||
@@ -214,11 +225,11 @@ Environment variables are also supported and would override the settings file
|
|||||||
For example in the settings.yml you have
|
For example in the settings.yml you have
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
system:
|
security:
|
||||||
enableLogin: 'true'
|
enableLogin: 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
To have this via an environment variable you would have ``SYSTEM_ENABLELOGIN``
|
To have this via an environment variable you would have ``SECURITY_ENABLELOGIN``
|
||||||
|
|
||||||
The Current list of settings is
|
The Current list of settings is
|
||||||
|
|
||||||
@@ -228,35 +239,37 @@ security:
|
|||||||
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
|
||||||
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)
|
||||||
@@ -267,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'])
|
||||||
@@ -303,7 +316,7 @@ For those wanting to use Stirling-PDFs backend API to link with their own custom
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 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.
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
| Technology | Ultra-Lite | Full |
|
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
|
||||||
|
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
|
||||||
|
|
||||||
|
Technology | Ultra-Lite | Full |
|
||||||
| ---------- | :--------: | :---: |
|
| ---------- | :--------: | :---: |
|
||||||
| Java | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ |
|
||||||
|
|||||||
241
build.gradle
@@ -1,24 +1,36 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id "java"
|
||||||
id 'org.springframework.boot' version '3.2.4'
|
id "org.springframework.boot" version "3.3.2"
|
||||||
id 'io.spring.dependency-management' version '1.1.3'
|
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.6'
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
group = 'stirling.software'
|
ext {
|
||||||
version = '0.25.2'
|
springBootVersion = "3.3.2"
|
||||||
|
pdfboxVersion = "3.0.3"
|
||||||
|
logbackVersion = "1.5.7"
|
||||||
|
imageioVersion = "3.11.0"
|
||||||
|
lombokVersion = "1.18.34"
|
||||||
|
bouncycastleVersion = "1.78.1"
|
||||||
|
}
|
||||||
|
|
||||||
//17 is lowest but we support and recommend 21
|
group = "stirling.software"
|
||||||
sourceCompatibility = '17'
|
version = "0.28.1"
|
||||||
|
|
||||||
|
java {
|
||||||
|
// 17 is lowest but we support and recommend 21
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
@@ -28,15 +40,19 @@ licenseReport {
|
|||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
java {
|
java {
|
||||||
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/web/AccountWebController.java'
|
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
|
||||||
exclude 'stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
|
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||||
exclude 'stirling/software/SPDF/model/Authority.java'
|
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||||
exclude 'stirling/software/SPDF/model/PersistentLogin.java'
|
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||||
exclude 'stirling/software/SPDF/model/User.java'
|
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
||||||
exclude 'stirling/software/SPDF/repository/**'
|
exclude "stirling/software/SPDF/model/Authority.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/repository/**"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,34 +65,34 @@ openApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
launch4j {
|
launch4j {
|
||||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||||
|
|
||||||
outfile="Stirling-PDF.exe"
|
outfile="Stirling-PDF.exe"
|
||||||
headerType="console"
|
headerType="console"
|
||||||
jarTask = tasks.bootJar
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 21?"
|
errTitle="Encountered error, Do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
||||||
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
|
||||||
jreMinVersion="17"
|
jreMinVersion="17"
|
||||||
|
|
||||||
mutexName="Stirling-PDF"
|
mutexName="Stirling-PDF"
|
||||||
windowTitle="Stirling-PDF"
|
windowTitle="Stirling-PDF"
|
||||||
|
|
||||||
messagesStartupError="An error occurred while starting Stirling-PDF"
|
messagesStartupError="An error occurred while starting Stirling-PDF"
|
||||||
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
// messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
|
||||||
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21."
|
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21."
|
||||||
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21."
|
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21."
|
||||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
java {
|
java {
|
||||||
target project.fileTree('src/main/java')
|
target project.fileTree('src/main/java')
|
||||||
|
|
||||||
googleJavaFormat('1.19.1').aosp().reorderImports(false)
|
googleJavaFormat("1.22.0").aosp().reorderImports(false)
|
||||||
|
|
||||||
importOrder('java', 'javax', 'org', 'com', 'net', 'io')
|
importOrder("java", "javax", "org", "com", "net", "io")
|
||||||
toggleOffOn()
|
toggleOffOn()
|
||||||
trimTrailingWhitespace()
|
trimTrailingWhitespace()
|
||||||
indentWithSpaces()
|
indentWithSpaces()
|
||||||
@@ -84,130 +100,143 @@ spotless {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.wrapper {
|
||||||
|
gradleVersion = "8.7"
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//security updates
|
//security updates
|
||||||
implementation 'ch.qos.logback:logback-classic:1.5.3'
|
implementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
implementation 'ch.qos.logback:logback-core:1.5.3'
|
implementation "ch.qos.logback:logback-core:$logbackVersion"
|
||||||
implementation 'org.springframework:spring-webmvc:6.1.5'
|
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 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
||||||
|
|
||||||
implementation 'org.yaml:snakeyaml:2.2'
|
|
||||||
|
|
||||||
// Exclude Tomcat and include Jetty
|
// Exclude Tomcat and include Jetty
|
||||||
implementation('org.springframework.boot:spring-boot-starter-web:3.2.4') {
|
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") {
|
||||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||||
}
|
}
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-jetty:3.2.4'
|
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
||||||
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.4'
|
|
||||||
|
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.4'
|
|
||||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
|
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.4"
|
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4'
|
implementation "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-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"
|
implementation "com.h2database:h2:2.1.214"
|
||||||
|
// implementation "com.h2database:h2:2.2.224"
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.4'
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
|
||||||
// Batik
|
// Batik
|
||||||
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'
|
implementation "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||||
implementation 'com.twelvemonkeys.imageio:imageio-bmp:3.10.1'
|
implementation "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-hdr:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-icns:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-iff:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
|
||||||
implementation 'com.twelvemonkeys.imageio:imageio-jpeg:3.10.1'
|
implementation "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-pcx:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-pict:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-pnm:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-psd:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-sgi:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-tga:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
||||||
implementation 'com.twelvemonkeys.imageio:imageio-tiff:3.10.1'
|
implementation "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
|
||||||
implementation 'com.twelvemonkeys.imageio:imageio-webp:3.10.1'
|
implementation "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
||||||
// implementation 'com.twelvemonkeys.imageio:imageio-xwd:3.10.1'
|
// implementation "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
||||||
|
|
||||||
implementation 'commons-io:commons-io:2.15.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
|
||||||
implementation ('com.opencsv:opencsv:5.9') {
|
implementation ("com.opencsv:opencsv:5.9") {
|
||||||
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.bouncycastle:bcprov-jdk18on:1.77'
|
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
|
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-actuator:3.2.4'
|
|
||||||
implementation 'io.micrometer:micrometer-core:1.12.4'
|
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
||||||
implementation group: 'com.google.zxing', name: 'core', version: '3.5.3'
|
|
||||||
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
|
implementation "io.micrometer:micrometer-core:1.13.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.github.vladimir-bukhtoyarov/bucket4j-core
|
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||||
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
|
implementation "com.bucket4j:bucket4j_jdk17-core:8.13.1"
|
||||||
|
implementation "com.fathzer:javaluator:3.0.4"
|
||||||
|
|
||||||
implementation 'com.fathzer:javaluator:3.0.3'
|
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||||
|
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.4")
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
|
||||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
dependsOn 'spotlessApply'
|
options.encoding = "UTF-8"
|
||||||
|
dependsOn "spotlessApply"
|
||||||
}
|
}
|
||||||
compileJava {
|
compileJava {
|
||||||
options.compilerArgs << '-parameters'
|
options.compilerArgs << "-parameters"
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('writeVersion') {
|
task writeVersion {
|
||||||
def propsFile = file('src/main/resources/version.properties')
|
def propsFile = file("src/main/resources/version.properties")
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
props.setProperty('version', version)
|
props.setProperty("version", version)
|
||||||
props.store(propsFile.newWriter(), null)
|
props.store(propsFile.newWriter(), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
swaggerhubUpload {
|
swaggerhubUpload {
|
||||||
//dependsOn generateOpenApiDocs // Depends on your task generating Swagger docs
|
//dependsOn generateOpenApiDocs // Depends on your task generating Swagger docs
|
||||||
api 'Stirling-PDF' // The name of your API on SwaggerHub
|
api "Stirling-PDF" // The name of your API on SwaggerHub
|
||||||
owner 'Frooodle' // Your SwaggerHub username (or organization name)
|
owner "Frooodle" // Your SwaggerHub username (or organization name)
|
||||||
version project.version // The version of your API
|
version project.version // The version of your API
|
||||||
inputFile './SwaggerDoc.json' // The path to your Swagger docs
|
inputFile "./SwaggerDoc.json" // The path to your Swagger docs
|
||||||
token "${System.getenv('SWAGGERHUB_API_KEY')}" // Your SwaggerHub API key, passed as an environment variable
|
token "${System.getenv("SWAGGERHUB_API_KEY")}" // Your SwaggerHub API key, passed as an environment variable
|
||||||
oas '3.0.0' // The version of the OpenAPI Specification you're using
|
oas "3.0.0" // The version of the OpenAPI Specification you"re using
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
enabled = false
|
enabled = false
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Implementation-Title': 'Stirling-PDF',
|
attributes "Implementation-Title": "Stirling-PDF",
|
||||||
'Implementation-Version': project.version
|
"Implementation-Version": project.version
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test') {
|
tasks.named("test") {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('printVersion') {
|
task printVersion {
|
||||||
println project.version
|
println project.version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.25.2
|
appVersion: 0.28.1
|
||||||
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
@@ -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
@@ -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
@@ -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
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
@@ -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
|
||||||
@@ -127,4 +127,4 @@ Feature: API Validation
|
|||||||
And the response PDF metadata should include "Title" as "Sample Title"
|
And the response PDF metadata should include "Title" as "Sample Title"
|
||||||
And the response status code should be 200
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,36 +75,75 @@ 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()
|
||||||
box_counter = 0
|
box_counter = 0
|
||||||
|
|
||||||
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()
|
||||||
can.save()
|
can.save()
|
||||||
packet.seek(0)
|
packet.seek(0)
|
||||||
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}'"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.4 KiB |
@@ -1,110 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512" xml:space="preserve"><defs id="defs173"><linearGradient id="XMLID_5_" x1="304.496" x2="316.036" y1="422.91" y2="326.263" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop156"/><stop offset="1" style="stop-color:#c2c2c9" id="stop158"/></linearGradient></defs><style id="style150" type="text/css">.st1{fill:#c02223}.st2{fill:#882425}.st3{fill:url(#XMLID_5_)}.st4{fill:url(#XMLID_7_)}</style><g id="XMLID_4_"><path id="XMLID_131_" d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z" class="st1" style="stroke-width:1.45391"/><path id="XMLID_117_" d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z" class="st2" style="stroke-width:1.45391"/><polygon id="XMLID_18_" points="234.7 422.6 368.5 387.7 393.5 262.2" class="st3" style="fill:url(#XMLID_5_)" transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)"/><linearGradient id="XMLID_7_" x1="223.084" x2="241.417" y1="372.756" y2="114.557" gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#dcf1f3" id="stop163"/><stop offset="1" style="stop-color:#c2c2c9" id="stop165"/></linearGradient><path id="XMLID_6_" d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z" class="st4" style="fill:url(#XMLID_7_);stroke-width:1.45391"/></g></svg>
|
||||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
id="Layer_1"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
style="enable-background:new 0 0 512 512;"
|
|
||||||
xml:space="preserve"
|
|
||||||
sodipodi:docname="favicon.svg"
|
|
||||||
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
|
||||||
inkscape:export-filename="favicon.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs173">
|
|
||||||
|
|
||||||
|
|
||||||
<linearGradient
|
|
||||||
id="XMLID_5_"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="304.496"
|
|
||||||
y1="422.9102"
|
|
||||||
x2="316.036"
|
|
||||||
y2="326.2626">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
style="stop-color:#DCF1F3"
|
|
||||||
id="stop156" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
style="stop-color:#C2C2C9"
|
|
||||||
id="stop158" />
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
</defs><sodipodi:namedview
|
|
||||||
id="namedview171"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="1.4142136"
|
|
||||||
inkscape:cx="219.91021"
|
|
||||||
inkscape:cy="232.63813"
|
|
||||||
inkscape:window-width="3840"
|
|
||||||
inkscape:window-height="2054"
|
|
||||||
inkscape:window-x="2869"
|
|
||||||
inkscape:window-y="-11"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="XMLID_4_" />
|
|
||||||
<style
|
|
||||||
type="text/css"
|
|
||||||
id="style150">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill:#C02223;}
|
|
||||||
.st2{fill:#882425;}
|
|
||||||
.st3{fill:url(#XMLID_5_);}
|
|
||||||
.st4{fill:url(#XMLID_7_);}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<g
|
|
||||||
id="XMLID_4_">
|
|
||||||
<path
|
|
||||||
id="XMLID_131_"
|
|
||||||
class="st1"
|
|
||||||
d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z"
|
|
||||||
sodipodi:nodetypes="ccssccccccccc"
|
|
||||||
style="stroke-width:1.45391" /><path
|
|
||||||
id="XMLID_117_"
|
|
||||||
class="st2"
|
|
||||||
d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z"
|
|
||||||
style="stroke-width:1.45391" /><polygon
|
|
||||||
id="XMLID_18_"
|
|
||||||
class="st3"
|
|
||||||
points="234.7,422.6 368.5,387.7 393.5,262.2 "
|
|
||||||
style="fill:url(#XMLID_5_)"
|
|
||||||
transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)" />
|
|
||||||
<linearGradient
|
|
||||||
id="XMLID_7_"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
x1="223.0838"
|
|
||||||
y1="372.7559"
|
|
||||||
x2="241.4174"
|
|
||||||
y2="114.557"
|
|
||||||
gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)">
|
|
||||||
<stop
|
|
||||||
offset="0"
|
|
||||||
style="stop-color:#DCF1F3"
|
|
||||||
id="stop163" />
|
|
||||||
<stop
|
|
||||||
offset="1"
|
|
||||||
style="stop-color:#C2C2C9"
|
|
||||||
id="stop165" />
|
|
||||||
</linearGradient>
|
|
||||||
<path
|
|
||||||
id="XMLID_6_"
|
|
||||||
class="st4"
|
|
||||||
d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z"
|
|
||||||
style="fill:url(#XMLID_7_);stroke-width:1.45391" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
BIN
images/settings-light.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 12 KiB |
BIN
images/stirling-home-dark.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 145 KiB |
43
pipeline/defaultWebUIConfigs/OCR images.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "OCR images",
|
||||||
|
"pipeline": [
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/convert/img/pdf",
|
||||||
|
"parameters": {
|
||||||
|
"fitOption": "fillPage",
|
||||||
|
"colorType": "color",
|
||||||
|
"autoRotate": true,
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/general/merge-pdfs",
|
||||||
|
"parameters": {
|
||||||
|
"sortType": "orderProvided",
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"operation": "/api/v1/misc/ocr-pdf",
|
||||||
|
"parameters": {
|
||||||
|
"languages": [
|
||||||
|
"eng"
|
||||||
|
],
|
||||||
|
"sidecar": false,
|
||||||
|
"deskew": false,
|
||||||
|
"clean": false,
|
||||||
|
"cleanFinal": false,
|
||||||
|
"ocrType": "skip-text",
|
||||||
|
"ocrRenderType": "hocr",
|
||||||
|
"removeImagesAfter": false,
|
||||||
|
"fileInput": "automated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"_examples": {
|
||||||
|
"outputDir": "{outputFolder}/{folderName}",
|
||||||
|
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
|
||||||
|
},
|
||||||
|
"outputDir": "{outputFolder}",
|
||||||
|
"outputFileName": "{filename}"
|
||||||
|
}
|
||||||
@@ -79,7 +79,9 @@ def write_readme(progress_list: list[tuple[str, int]]) -> None:
|
|||||||
file.writelines(content)
|
file.writelines(content)
|
||||||
|
|
||||||
|
|
||||||
def compare_files(default_file_path, file_paths, translation_status_file) -> list[tuple[str, int]]:
|
def compare_files(
|
||||||
|
default_file_path, file_paths, ignore_translation_file
|
||||||
|
) -> list[tuple[str, int]]:
|
||||||
"""Compares the default properties file with other
|
"""Compares the default properties file with other
|
||||||
properties files in the directory.
|
properties files in the directory.
|
||||||
|
|
||||||
@@ -92,18 +94,24 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
|
|||||||
language and progress percentage.
|
language and progress percentage.
|
||||||
""" # noqa: D205
|
""" # noqa: D205
|
||||||
num_lines = sum(
|
num_lines = sum(
|
||||||
1 for line in open(default_file_path, encoding="utf-8") if line.strip() and not line.strip().startswith("#")
|
1
|
||||||
|
for line in open(default_file_path, encoding="utf-8")
|
||||||
|
if line.strip() and not line.strip().startswith("#")
|
||||||
)
|
)
|
||||||
|
|
||||||
result_list = []
|
result_list = []
|
||||||
sort_translation_status: tomlkit.TOMLDocument
|
sort_ignore_translation: tomlkit.TOMLDocument
|
||||||
|
|
||||||
# read toml
|
# read toml
|
||||||
with open(translation_status_file, encoding="utf-8") as f:
|
with open(ignore_translation_file, encoding="utf-8") as f:
|
||||||
sort_translation_status = tomlkit.parse(f.read())
|
sort_ignore_translation = tomlkit.parse(f.read())
|
||||||
|
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
language = os.path.basename(file_path).split("messages_", 1)[1].split(".properties", 1)[0]
|
language = (
|
||||||
|
os.path.basename(file_path)
|
||||||
|
.split("messages_", 1)[1]
|
||||||
|
.split(".properties", 1)[0]
|
||||||
|
)
|
||||||
|
|
||||||
fails = 0
|
fails = 0
|
||||||
if "en_GB" in language or "en_US" in language:
|
if "en_GB" in language or "en_US" in language:
|
||||||
@@ -111,21 +119,25 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
|
|||||||
result_list.append(("en_US", 100))
|
result_list.append(("en_US", 100))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if language not in sort_translation_status:
|
if language not in sort_ignore_translation:
|
||||||
sort_translation_status[language] = tomlkit.table()
|
sort_ignore_translation[language] = tomlkit.table()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
"ignore" not in sort_translation_status[language]
|
"ignore" not in sort_ignore_translation[language]
|
||||||
or len(sort_translation_status[language].get("ignore", [])) < 1
|
or len(sort_ignore_translation[language].get("ignore", [])) < 1
|
||||||
):
|
):
|
||||||
sort_translation_status[language]["ignore"] = tomlkit.array(["language.direction"])
|
sort_ignore_translation[language]["ignore"] = tomlkit.array(
|
||||||
|
["language.direction"]
|
||||||
|
)
|
||||||
|
|
||||||
# if "missing" not in sort_translation_status[language]:
|
# if "missing" not in sort_ignore_translation[language]:
|
||||||
# sort_translation_status[language]["missing"] = tomlkit.array()
|
# sort_ignore_translation[language]["missing"] = tomlkit.array()
|
||||||
# elif "language.direction" in sort_translation_status[language]["missing"]:
|
# elif "language.direction" in sort_ignore_translation[language]["missing"]:
|
||||||
# sort_translation_status[language]["missing"].remove("language.direction")
|
# sort_ignore_translation[language]["missing"].remove("language.direction")
|
||||||
|
|
||||||
with open(default_file_path, encoding="utf-8") as default_file, open(file_path, encoding="utf-8") as file:
|
with open(default_file_path, encoding="utf-8") as default_file, open(
|
||||||
|
file_path, encoding="utf-8"
|
||||||
|
) as file:
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
next(default_file)
|
next(default_file)
|
||||||
try:
|
try:
|
||||||
@@ -133,34 +145,45 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
fails = num_lines
|
fails = num_lines
|
||||||
|
|
||||||
for line_num, (line_default, line_file) in enumerate(zip(default_file, file), start=6):
|
for line_num, (line_default, line_file) in enumerate(
|
||||||
|
zip(default_file, file), start=6
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
# Ignoring empty lines and lines start with #
|
# Ignoring empty lines and lines start with #
|
||||||
if line_default.strip() == "" or line_default.startswith("#"):
|
if line_default.strip() == "" or line_default.startswith("#"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
default_key, default_value = line_default.split("=", 1)
|
default_key, default_value = line_default.split("=", 1)
|
||||||
file_key, file_value = line_file.split("=", 1)
|
file_key, file_value = line_file.split("=", 1)
|
||||||
if (
|
if (
|
||||||
default_value.strip() == file_value.strip()
|
default_value.strip() == file_value.strip()
|
||||||
and default_key.strip() not in sort_translation_status[language]["ignore"]
|
and default_key.strip()
|
||||||
|
not in sort_ignore_translation[language]["ignore"]
|
||||||
):
|
):
|
||||||
print(f"{language}: Line {line_num} is missing the translation.")
|
print(
|
||||||
# if default_key.strip() not in sort_translation_status[language]["missing"]:
|
f"{language}: Line {line_num} is missing the translation."
|
||||||
|
)
|
||||||
|
# if default_key.strip() not in sort_ignore_translation[language]["missing"]:
|
||||||
# missing_array = tomlkit.array()
|
# missing_array = tomlkit.array()
|
||||||
# missing_array.append(default_key.strip())
|
# missing_array.append(default_key.strip())
|
||||||
# missing_array.multiline(True)
|
# missing_array.multiline(True)
|
||||||
# sort_translation_status[language]["missing"].extend(missing_array)
|
# sort_ignore_translation[language]["missing"].extend(missing_array)
|
||||||
fails += 1
|
fails += 1
|
||||||
# elif default_key.strip() in sort_translation_status[language]["ignore"]:
|
# elif default_key.strip() in sort_ignore_translation[language]["ignore"]:
|
||||||
# if default_key.strip() in sort_translation_status[language]["missing"]:
|
# if default_key.strip() in sort_ignore_translation[language]["missing"]:
|
||||||
# sort_translation_status[language]["missing"].remove(default_key.strip())
|
# sort_ignore_translation[language]["missing"].remove(default_key.strip())
|
||||||
if default_value.strip() != file_value.strip():
|
if default_value.strip() != file_value.strip():
|
||||||
# if default_key.strip() in sort_translation_status[language]["missing"]:
|
# if default_key.strip() in sort_ignore_translation[language]["missing"]:
|
||||||
# sort_translation_status[language]["missing"].remove(default_key.strip())
|
# sort_ignore_translation[language]["missing"].remove(default_key.strip())
|
||||||
if default_key.strip() in sort_translation_status[language]["ignore"]:
|
if (
|
||||||
sort_translation_status[language]["ignore"].remove(default_key.strip())
|
default_key.strip()
|
||||||
|
in sort_ignore_translation[language]["ignore"]
|
||||||
|
):
|
||||||
|
sort_ignore_translation[language]["ignore"].remove(
|
||||||
|
default_key.strip()
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
print(f"{line_default}|{line_file}")
|
||||||
|
exit(1)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -171,9 +194,9 @@ def compare_files(default_file_path, file_paths, translation_status_file) -> lis
|
|||||||
int((num_lines - fails) * 100 / num_lines),
|
int((num_lines - fails) * 100 / num_lines),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
translation_status = convert_to_multiline(sort_translation_status)
|
ignore_translation = convert_to_multiline(sort_ignore_translation)
|
||||||
with open(translation_status_file, "w", encoding="utf-8") as file:
|
with open(ignore_translation_file, "w", encoding="utf-8") as file:
|
||||||
file.write(tomlkit.dumps(translation_status))
|
file.write(tomlkit.dumps(ignore_translation))
|
||||||
|
|
||||||
unique_data = list(set(result_list))
|
unique_data = list(set(result_list))
|
||||||
unique_data.sort(key=lambda x: x[1], reverse=True)
|
unique_data.sort(key=lambda x: x[1], reverse=True)
|
||||||
@@ -187,6 +210,8 @@ if __name__ == "__main__":
|
|||||||
reference_file = os.path.join(directory, "messages_en_GB.properties")
|
reference_file = os.path.join(directory, "messages_en_GB.properties")
|
||||||
|
|
||||||
scripts_directory = os.path.join(os.getcwd(), "scripts")
|
scripts_directory = os.path.join(os.getcwd(), "scripts")
|
||||||
translation_state_file = os.path.join(scripts_directory, "translation_status.toml")
|
translation_state_file = os.path.join(scripts_directory, "ignore_translation.toml")
|
||||||
|
|
||||||
write_readme(compare_files(reference_file, messages_file_paths, translation_state_file))
|
write_readme(
|
||||||
|
compare_files(reference_file, messages_file_paths, translation_state_file)
|
||||||
|
)
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -59,6 +69,30 @@ ignore = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[fr_FR]
|
[fr_FR]
|
||||||
|
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',
|
||||||
|
'licenses.license',
|
||||||
|
'licenses.module',
|
||||||
|
'licenses.nav',
|
||||||
|
'licenses.version',
|
||||||
|
'pdfOrganiser.mode',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'sponsor',
|
||||||
|
'watermark.type.2',
|
||||||
|
]
|
||||||
|
|
||||||
|
[ga_IE]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
@@ -70,6 +104,7 @@ ignore = [
|
|||||||
|
|
||||||
[hr_HR]
|
[hr_HR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'PDFToBook.selectText.1',
|
||||||
'font',
|
'font',
|
||||||
'home.pipeline.title',
|
'home.pipeline.title',
|
||||||
'info',
|
'info',
|
||||||
@@ -113,16 +148,34 @@ ignore = [
|
|||||||
|
|
||||||
[nl_NL]
|
[nl_NL]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'HTMLToPDF.print',
|
||||||
|
'adjustContrast.contrast',
|
||||||
|
'compare.document.1',
|
||||||
|
'compare.document.2',
|
||||||
|
'error',
|
||||||
|
'getPdfInfo.downloadJson',
|
||||||
|
'help',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'navbar.allTools',
|
||||||
|
'printFile.submit',
|
||||||
|
'showJS.downloadJS',
|
||||||
|
'sponsor',
|
||||||
]
|
]
|
||||||
|
|
||||||
[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',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -148,12 +201,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]
|
||||||
@@ -161,6 +222,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',
|
||||||
@@ -171,6 +240,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
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -79,13 +78,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.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("&");
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,8 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
// Redirect to the URL with only allowed query parameters
|
// Redirect to the URL with only allowed query parameters
|
||||||
String redirectUrl = requestURI + "?" + newQueryString;
|
String redirectUrl = requestURI + "?" + newQueryString;
|
||||||
response.sendRedirect(redirectUrl);
|
|
||||||
|
response.sendRedirect(request.getContextPath() + redirectUrl);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,26 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.simpleyaml.configuration.comments.CommentType;
|
||||||
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
public class ConfigInitializer
|
public class ConfigInitializer
|
||||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
try {
|
try {
|
||||||
@@ -44,95 +53,89 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Path templatePath =
|
|
||||||
// Paths.get(
|
// Define the path to the config settings file
|
||||||
// getClass()
|
Path settingsPath = Paths.get("configs", "settings.yml");
|
||||||
// .getClassLoader()
|
// Load the template resource
|
||||||
// .getResource("settings.yml.template")
|
URL settingsTemplateResource =
|
||||||
// .toURI());
|
getClass().getClassLoader().getResource("settings.yml.template");
|
||||||
// Path userPath = Paths.get("configs", "settings.yml");
|
if (settingsTemplateResource == null) {
|
||||||
//
|
throw new IOException("Resource not found: settings.yml.template");
|
||||||
// List<String> templateLines = Files.readAllLines(templatePath);
|
}
|
||||||
// List<String> userLines =
|
|
||||||
// Files.exists(userPath) ? Files.readAllLines(userPath) : new
|
// Create a temporary file to copy the resource content
|
||||||
// ArrayList<>();
|
Path tempTemplatePath = Files.createTempFile("settings.yml", ".template");
|
||||||
//
|
|
||||||
// List<String> resultLines = new ArrayList<>();
|
try (InputStream in = settingsTemplateResource.openStream()) {
|
||||||
// int position = 0;
|
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
// for (String templateLine : templateLines) {
|
}
|
||||||
// // Check if the line is a comment
|
|
||||||
// if (templateLine.trim().startsWith("#")) {
|
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
||||||
// String entry = templateLine.trim().substring(1).trim();
|
settingsTemplateFile.loadWithComments();
|
||||||
// if (!entry.isEmpty()) {
|
|
||||||
// // Check if this comment has been uncommented in userLines
|
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
||||||
// String key = entry.split(":")[0].trim();
|
settingsFile.loadWithComments();
|
||||||
// addLine(resultLines, userLines, templateLine, key, position);
|
|
||||||
// } else {
|
// Load headers and comments
|
||||||
// resultLines.add(templateLine);
|
String header = settingsTemplateFile.getHeader();
|
||||||
// }
|
|
||||||
// }
|
// Create a new file for temporary settings
|
||||||
// // Check if the line is a key-value pair
|
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
||||||
// else if (templateLine.contains(":")) {
|
tempSettingFile.createNewFile(true);
|
||||||
// String key = templateLine.split(":")[0].trim();
|
tempSettingFile.setHeader(header);
|
||||||
// addLine(resultLines, userLines, templateLine, key, position);
|
|
||||||
// }
|
// Get all keys from the template
|
||||||
// // Handle empty lines
|
List<String> keys =
|
||||||
// else if (templateLine.trim().length() == 0) {
|
Arrays.asList(settingsTemplateFile.getKeys(true).toArray(new String[0]));
|
||||||
// resultLines.add("");
|
|
||||||
// }
|
for (String key : keys) {
|
||||||
// position++;
|
if (!key.contains(".")) {
|
||||||
// }
|
// Add blank lines and comments to specific sections
|
||||||
//
|
tempSettingFile
|
||||||
// // Write the result to the user settings file
|
.path(key)
|
||||||
// Files.write(userPath, resultLines);
|
.comment(settingsTemplateFile.getComment(key))
|
||||||
|
.blankLine();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Copy settings from the template to the settings.yml file
|
||||||
|
changeConfigItemFromCommentToKeyValue(
|
||||||
|
settingsTemplateFile, settingsFile, tempSettingFile, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the settings.yml file
|
||||||
|
tempSettingFile.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create custom settings file if it doesn't exist
|
||||||
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||||
if (!Files.exists(customSettingsPath)) {
|
if (!Files.exists(customSettingsPath)) {
|
||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
private void changeConfigItemFromCommentToKeyValue(
|
||||||
private static void addLine(
|
final YamlFile settingsTemplateFile,
|
||||||
List<String> resultLines,
|
final YamlFile settingsFile,
|
||||||
List<String> userLines,
|
final YamlFile tempSettingFile,
|
||||||
String templateLine,
|
String path) {
|
||||||
String key,
|
if (settingsFile.get(path) == null && settingsTemplateFile.get(path) != null) {
|
||||||
int position) {
|
// If the key is only in the template, add it to the temporary settings with comments
|
||||||
boolean added = false;
|
tempSettingFile
|
||||||
int templateIndentationLevel = getIndentationLevel(templateLine);
|
.path(path)
|
||||||
int pos = 0;
|
.set(settingsTemplateFile.get(path))
|
||||||
for (String settingsLine : userLines) {
|
.comment(settingsTemplateFile.getComment(path, CommentType.BLOCK))
|
||||||
if (settingsLine.trim().startsWith(key + ":") && position == pos) {
|
.commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
|
||||||
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
} else if (settingsFile.get(path) != null && settingsTemplateFile.get(path) != null) {
|
||||||
// Check if it is correct settingsLine and has the same parent as templateLine
|
// If the key is in both, update the temporary settings with the main settings' value
|
||||||
if (settingsIndentationLevel == templateIndentationLevel) {
|
// and comments
|
||||||
resultLines.add(settingsLine);
|
tempSettingFile
|
||||||
added = true;
|
.path(path)
|
||||||
break;
|
.set(settingsFile.get(path))
|
||||||
}
|
.comment(settingsTemplateFile.getComment(path, CommentType.BLOCK))
|
||||||
}
|
.commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
|
||||||
pos++;
|
} else {
|
||||||
|
// Log if the key is not found in both YAML files
|
||||||
|
logger.info("Key not found in both YAML files: " + path);
|
||||||
}
|
}
|
||||||
if (!added) {
|
|
||||||
resultLines.add(templateLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getIndentationLevel(String line) {
|
|
||||||
int indentationLevel = 0;
|
|
||||||
String trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.startsWith("#")) {
|
|
||||||
line = trimmedLine.substring(1);
|
|
||||||
}
|
|
||||||
for (char c : line.toCharArray()) {
|
|
||||||
if (c == ' ') {
|
|
||||||
indentationLevel++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return indentationLevel;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -221,6 +222,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");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class AppUpdateAuthService implements ShowAdminInterface {
|
|||||||
@Autowired private UserRepository userRepository;
|
@Autowired private UserRepository userRepository;
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean getShowUpdateOnlyAdmins() {
|
public boolean getShowUpdateOnlyAdmins() {
|
||||||
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
|
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
|
||||||
if (!showUpdate) {
|
if (!showUpdate) {
|
||||||
|
|||||||
@@ -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,12 +37,17 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
if (exception instanceof DisabledException) {
|
||||||
logger.error("Failed login attempt from IP: {}", ip);
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
String ip = request.getRemoteAddr();
|
||||||
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
log.error("Failed login attempt from IP: {}", ip);
|
||||||
response.sendRedirect("/login?error=oauth2AuthenticationError");
|
|
||||||
|
if (exception instanceof LockedException) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,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("/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("/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
|
||||||
@@ -37,7 +46,8 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (savedRequest != null
|
if (savedRequest != null
|
||||||
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
&& !RequestUriUtils.isStaticResource(
|
||||||
|
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,10 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
String method = request.getMethod();
|
String method = request.getMethod();
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
|
||||||
// Check if the request is for static resources
|
// Check if the request is for static resources
|
||||||
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|
boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI);
|
||||||
|
|
||||||
// If it's a static resource, just continue the filter chain and skip the logic below
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
if (isStaticResource) {
|
if (isStaticResource) {
|
||||||
@@ -43,8 +45,8 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
if ("GET".equalsIgnoreCase(method)
|
if ("GET".equalsIgnoreCase(method)
|
||||||
&& user.isPresent()
|
&& user.isPresent()
|
||||||
&& user.get().isFirstLogin()
|
&& user.get().isFirstLogin()
|
||||||
&& !"/change-creds".equals(requestURI)) {
|
&& !(contextPath + "/change-creds").equals(requestURI)) {
|
||||||
response.sendRedirect("/change-creds");
|
response.sendRedirect(contextPath + "/change-creds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class IPRateLimitingFilter implements Filter {
|
|||||||
String method = httpRequest.getMethod();
|
String method = httpRequest.getMethod();
|
||||||
String requestURI = httpRequest.getRequestURI();
|
String requestURI = httpRequest.getRequestURI();
|
||||||
// Check if the request is for static resources
|
// Check if the request is for static resources
|
||||||
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|
boolean isStaticResource =
|
||||||
|
RequestUriUtils.isStaticResource(httpRequest.getContextPath(), requestURI);
|
||||||
|
|
||||||
// If it's a static resource, just continue the filter chain and skip the logic below
|
// If it's a static resource, just continue the filter chain and skip the logic below
|
||||||
if (isStaticResource) {
|
if (isStaticResource) {
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.List;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
@@ -42,12 +46,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
|
||||||
@@ -55,9 +58,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 {
|
||||||
@@ -65,54 +68,38 @@ 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveKeyToConfig(String key) throws IOException {
|
private void saveKeyToConfig(String key) throws IOException {
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
List<String> lines = Files.readAllLines(path);
|
|
||||||
boolean keyFound = false;
|
|
||||||
|
|
||||||
// Search for the existing key to replace it or place to add it
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
for (int i = 0; i < lines.size(); i++) {
|
|
||||||
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
|
|
||||||
keyFound = true;
|
|
||||||
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
|
|
||||||
lines.set(i + 1, " key: " + key);
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
lines.add(i + 1, " key: " + key);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the section doesn't exist, append it
|
settingsYml.loadWithComments();
|
||||||
if (!keyFound) {
|
|
||||||
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
|
|
||||||
lines.add("AutomaticallyGenerated:");
|
|
||||||
lines.add(" key: " + key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write back to the file
|
settingsYml
|
||||||
Files.write(path, lines);
|
.path("AutomaticallyGenerated.key")
|
||||||
|
.set(key)
|
||||||
|
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
|
settingsYml.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidUUID(String uuid) {
|
private boolean isValidUUID(String uuid) {
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.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;
|
||||||
|
|
||||||
@@ -17,8 +15,6 @@ public class LoginAttemptService {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired 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;
|
||||||
|
|||||||
@@ -18,8 +18,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,17 +35,18 @@ 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.GithubProvider;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
|
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 +72,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 +89,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 +98,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(
|
||||||
@@ -160,7 +155,11 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
// 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 ->
|
||||||
@@ -191,10 +190,8 @@ public class SecurityConfiguration {
|
|||||||
.logout(
|
.logout(
|
||||||
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())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 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;
|
||||||
@@ -9,8 +10,9 @@ 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.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 +20,16 @@ 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;
|
||||||
|
|
||||||
@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;
|
||||||
@@ -87,6 +90,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +144,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
contextPath + "/fonts/",
|
contextPath + "/fonts/",
|
||||||
contextPath + "/js/",
|
contextPath + "/js/",
|
||||||
contextPath + "/pdfjs/",
|
contextPath + "/pdfjs/",
|
||||||
|
contextPath + "/pdfjs-legacy/",
|
||||||
contextPath + "/api/v1/info/status",
|
contextPath + "/api/v1/info/status",
|
||||||
contextPath + "/site.webmanifest"
|
contextPath + "/site.webmanifest"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
import io.github.bucket4j.Bandwidth;
|
import io.github.bucket4j.Bandwidth;
|
||||||
import io.github.bucket4j.Bucket;
|
import io.github.bucket4j.Bucket;
|
||||||
import io.github.bucket4j.ConsumptionProbe;
|
import io.github.bucket4j.ConsumptionProbe;
|
||||||
import io.github.bucket4j.Refill;
|
|
||||||
import io.github.pixee.security.Newlines;
|
import io.github.pixee.security.Newlines;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
@@ -142,7 +141,10 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
private Bucket createUserBucket(int limitPerDay) {
|
private Bucket createUserBucket(int limitPerDay) {
|
||||||
Bandwidth limit =
|
Bandwidth limit =
|
||||||
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
|
Bandwidth.builder()
|
||||||
|
.capacity(limitPerDay)
|
||||||
|
.refillIntervally(limitPerDay, Duration.ofDays(1))
|
||||||
|
.build();
|
||||||
return Bucket.builder().addLimit(limit).build();
|
return Bucket.builder().addLimit(limit).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -14,11 +15,15 @@ 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;
|
||||||
@@ -38,12 +43,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;
|
||||||
}
|
}
|
||||||
@@ -85,8 +95,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
userRepository
|
findByUsernameIgnoreCase(username)
|
||||||
.findByUsernameIgnoreCase(username)
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
|
||||||
user.setApiKey(generateApiKey());
|
user.setApiKey(generateApiKey());
|
||||||
@@ -99,8 +108,7 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -126,12 +134,17 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 +152,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 +169,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 +185,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 +203,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 = findByUsernameIgnoreCase(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 +237,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setSettings(settingsMap);
|
user.setSettings(settingsMap);
|
||||||
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,28 +253,39 @@ public class UserService implements UserServiceInterface {
|
|||||||
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 +309,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);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
logger.error("Unhandled authentication exception", exception);
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
|
||||||
}
|
}
|
||||||
|
log.error("Unhandled authentication exception", 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,23 +42,32 @@ 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();
|
||||||
SavedRequest savedRequest =
|
SavedRequest savedRequest =
|
||||||
(session != null)
|
(session != null)
|
||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (savedRequest != null
|
if (savedRequest != null
|
||||||
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
&& !RequestUriUtils.isStaticResource(contextPath, savedRequest.getRedirectUrl())) {
|
||||||
// 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");
|
||||||
@@ -75,18 +80,23 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
&& !userService.isAuthenticationTypeByUsername(
|
&& !userService.isAuthenticationTypeByUsername(
|
||||||
username, AuthenticationType.OAUTH2)
|
username, AuthenticationType.OAUTH2)
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
request.getContextPath() + "/logout?oauth2AuthenticationErrorWeb=true");
|
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
try {
|
try {
|
||||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
if (oAuth.getBlockRegistration()
|
||||||
response.sendRedirect("/");
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
return;
|
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.sendRedirect("/logout?invalidUsername=true");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
}
|
||||||
|
response.sendRedirect(contextPath + "/");
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +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.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,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) {
|
||||||
@@ -51,10 +53,9 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
Provider provider = oauth.getClient().get(registrationId);
|
Provider provider = oauth.getClient().get(registrationId);
|
||||||
issuer = provider.getIssuer();
|
issuer = provider.getIssuer();
|
||||||
clientId = provider.getClientId();
|
clientId = provider.getClientId();
|
||||||
} catch (Exception e) {
|
} catch (UnsupportedProviderException e) {
|
||||||
logger.error("exception", e);
|
log.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
issuer = oauth.getIssuer();
|
issuer = oauth.getIssuer();
|
||||||
@@ -69,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
|
||||||
@@ -91,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":
|
||||||
@@ -105,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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,16 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
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;
|
||||||
@@ -38,6 +43,7 @@ public class MergeController {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
|
||||||
|
|
||||||
|
// Merges a list of PDDocument objects into a single PDDocument
|
||||||
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
PDDocument mergedDoc = new PDDocument();
|
PDDocument mergedDoc = new PDDocument();
|
||||||
for (PDDocument doc : documents) {
|
for (PDDocument doc : documents) {
|
||||||
@@ -48,6 +54,7 @@ public class MergeController {
|
|||||||
return mergedDoc;
|
return mergedDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns a comparator for sorting MultipartFile arrays based on the given sort type
|
||||||
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
||||||
switch (sortType) {
|
switch (sortType) {
|
||||||
case "byFileName":
|
case "byFileName":
|
||||||
@@ -108,37 +115,78 @@ public class MergeController {
|
|||||||
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
|
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
|
||||||
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
|
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
List<File> filesToDelete = new ArrayList<File>();
|
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
||||||
|
ByteArrayOutputStream docOutputstream =
|
||||||
|
new ByteArrayOutputStream(); // Stream for the merged document
|
||||||
|
PDDocument mergedDocument = null;
|
||||||
|
|
||||||
|
boolean removeCertSign = form.isRemoveCertSign();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
MultipartFile[] files = form.getFileInput();
|
MultipartFile[] files = form.getFileInput();
|
||||||
Arrays.sort(files, getSortComparator(form.getSortType()));
|
Arrays.sort(
|
||||||
|
files,
|
||||||
PDFMergerUtility mergedDoc = new PDFMergerUtility();
|
getSortComparator(
|
||||||
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
|
form.getSortType())); // Sort files based on the given sort type
|
||||||
|
|
||||||
|
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||||
for (MultipartFile multipartFile : files) {
|
for (MultipartFile multipartFile : files) {
|
||||||
File tempFile = GeneralUtils.convertMultipartFileToFile(multipartFile);
|
File tempFile =
|
||||||
filesToDelete.add(tempFile);
|
GeneralUtils.convertMultipartFileToFile(
|
||||||
mergedDoc.addSource(tempFile);
|
multipartFile); // Convert MultipartFile to File
|
||||||
|
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
||||||
|
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
||||||
|
}
|
||||||
|
mergerUtility.setDestinationStream(
|
||||||
|
docOutputstream); // Set the output stream for the merged document
|
||||||
|
mergerUtility.mergeDocuments(null); // Merge the documents
|
||||||
|
|
||||||
|
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
|
||||||
|
|
||||||
|
// Load the merged PDF document
|
||||||
|
mergedDocument = Loader.loadPDF(mergedPdfBytes);
|
||||||
|
|
||||||
|
// Remove signatures if removeCertSign is true
|
||||||
|
if (removeCertSign) {
|
||||||
|
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
||||||
|
PDAcroForm acroForm = catalog.getAcroForm();
|
||||||
|
if (acroForm != null) {
|
||||||
|
List<PDField> fieldsToRemove =
|
||||||
|
acroForm.getFields().stream()
|
||||||
|
.filter(field -> field instanceof PDSignatureField)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
|
acroForm.flatten(
|
||||||
|
fieldsToRemove,
|
||||||
|
false); // Flatten the fields, effectively removing them
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mergedDoc.setDestinationFileName(
|
// Save the modified document to a new ByteArrayOutputStream
|
||||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
mergedDoc.setDestinationStream(docOutputstream);
|
mergedDocument.save(baos);
|
||||||
|
|
||||||
mergedDoc.mergeDocuments(null);
|
|
||||||
|
|
||||||
|
String mergedFileName =
|
||||||
|
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_merged_unsigned.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
|
baos.toByteArray(), mergedFileName); // Return the modified PDF
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
logger.error("Error in merge pdf process", ex);
|
logger.error("Error in merge pdf process", ex);
|
||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
for (File file : filesToDelete) {
|
for (File file : filesToDelete) {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
Files.deleteIfExists(file.toPath());
|
Files.deleteIfExists(file.toPath()); // Delete temporary files
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
docOutputstream.close();
|
||||||
|
if (mergedDocument != null) {
|
||||||
|
mergedDocument.close(); // Close the merged document
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,6 +145,28 @@ public class RearrangePagesPDFController {
|
|||||||
return newPageOrder;
|
return newPageOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rearrange pages in a PDF file by merging odd and even pages. The first half of the pages will
|
||||||
|
* be the odd pages, and the second half will be the even pages as input. <br>
|
||||||
|
* This method is visible for testing purposes only.
|
||||||
|
*
|
||||||
|
* @param totalPages Total number of pages in the PDF file.
|
||||||
|
* @return List of page numbers in the new order. The first page is 0.
|
||||||
|
*/
|
||||||
|
List<Integer> oddEvenMerge(int totalPages) {
|
||||||
|
List<Integer> newPageOrderZeroBased = new ArrayList<>();
|
||||||
|
int numberOfOddPages = (totalPages + 1) / 2;
|
||||||
|
|
||||||
|
for (int oneBasedIndex = 1; oneBasedIndex < (numberOfOddPages + 1); oneBasedIndex++) {
|
||||||
|
newPageOrderZeroBased.add((oneBasedIndex - 1));
|
||||||
|
if (numberOfOddPages + oneBasedIndex <= totalPages) {
|
||||||
|
newPageOrderZeroBased.add((numberOfOddPages + oneBasedIndex - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPageOrderZeroBased;
|
||||||
|
}
|
||||||
|
|
||||||
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
|
private List<Integer> processSortTypes(String sortTypes, int totalPages) {
|
||||||
try {
|
try {
|
||||||
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase());
|
||||||
@@ -159,6 +181,8 @@ public class RearrangePagesPDFController {
|
|||||||
return sideStitchBooklet(totalPages);
|
return sideStitchBooklet(totalPages);
|
||||||
case ODD_EVEN_SPLIT:
|
case ODD_EVEN_SPLIT:
|
||||||
return oddEvenSplit(totalPages);
|
return oddEvenSplit(totalPages);
|
||||||
|
case ODD_EVEN_MERGE:
|
||||||
|
return oddEvenMerge(totalPages);
|
||||||
case REMOVE_FIRST:
|
case REMOVE_FIRST:
|
||||||
return removeFirst(totalPages);
|
return removeFirst(totalPages);
|
||||||
case REMOVE_LAST:
|
case REMOVE_LAST:
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -59,11 +66,12 @@ public class UserController {
|
|||||||
@PostMapping("/change-username")
|
@PostMapping("/change-username")
|
||||||
public RedirectView changeUsername(
|
public RedirectView changeUsername(
|
||||||
Principal principal,
|
Principal principal,
|
||||||
@RequestParam(name = "currentPassword") String currentPassword,
|
@RequestParam(name = "currentPasswordChangeUsername") String currentPassword,
|
||||||
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.saveUser(username, password, role, forceChange);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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<>();
|
||||||
// Iterate over each page
|
|
||||||
for (PDPage page : document.getPages()) {
|
|
||||||
++pageNum;
|
|
||||||
// Extract images from page
|
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
|
||||||
if (page.getResources().isImageXObject(name)) {
|
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
|
||||||
int imageHash = image.hashCode();
|
|
||||||
if (processedImages.contains(imageHash)) {
|
|
||||||
continue; // Skip already processed images
|
|
||||||
}
|
|
||||||
processedImages.add(imageHash);
|
|
||||||
|
|
||||||
// Convert image to desired format
|
if (useMultithreading) {
|
||||||
RenderedImage renderedImage = image.getImage();
|
// Executor service to handle multithreading
|
||||||
BufferedImage bufferedImage = null;
|
ExecutorService executor =
|
||||||
if ("png".equalsIgnoreCase(format)) {
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
bufferedImage =
|
Set<Future<Void>> futures = new HashSet<>();
|
||||||
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
|
// Iterate over each page
|
||||||
String imageName =
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
PDPage page = document.getPage(pgNum);
|
||||||
ZipEntry zipEntry = new ZipEntry(imageName);
|
int pageNum = document.getPages().indexOf(page) + 1;
|
||||||
zos.putNextEntry(zipEntry);
|
// Submit a task for processing each page
|
||||||
|
Future<Void> future =
|
||||||
|
executor.submit(
|
||||||
|
() -> {
|
||||||
|
extractImagesFromPage(
|
||||||
|
page, format, filename, pageNum, processedImages, zos);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
Graphics2D g = bufferedImage.createGraphics();
|
futures.add(future);
|
||||||
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();
|
// Wait for all tasks to complete
|
||||||
imageIndex++;
|
for (Future<Void> future : futures) {
|
||||||
}
|
future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,69 @@ 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 {
|
||||||
|
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();
|
||||||
|
|||||||
@@ -105,7 +105,14 @@ public class PipelineProcessor {
|
|||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
body.add(entry.getKey(), entry.getValue());
|
if (entry.getValue() instanceof List) {
|
||||||
|
List<?> list = (List<?>) entry.getValue();
|
||||||
|
for (Object item : list) {
|
||||||
|
body.add(entry.getKey(), item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
@@ -167,7 +174,14 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
body.add(entry.getKey(), entry.getValue());
|
if (entry.getValue() instanceof List) {
|
||||||
|
List<?> list = (List<?>) entry.getValue();
|
||||||
|
for (Object item : list) {
|
||||||
|
body.add(entry.getKey(), item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|||||||
@@ -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,15 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
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;
|
||||||
@@ -23,26 +25,35 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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.GithubProvider;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
|
|
||||||
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.Authority;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@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:/";
|
||||||
}
|
}
|
||||||
@@ -72,8 +83,13 @@ public class AccountWebController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Remove any null keys/values from the providerList
|
||||||
|
providerList
|
||||||
|
.entrySet()
|
||||||
|
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||||
model.addAttribute("providerlist", providerList);
|
model.addAttribute("providerlist", providerList);
|
||||||
|
|
||||||
|
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
|
||||||
model.addAttribute(
|
model.addAttribute(
|
||||||
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||||
|
|
||||||
@@ -131,6 +147,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;
|
||||||
}
|
}
|
||||||
@@ -149,9 +172,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(
|
||||||
@@ -160,6 +180,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) {
|
||||||
@@ -170,9 +197,73 @@ 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");
|
||||||
|
|
||||||
String deleteMessage = null;
|
String deleteMessage = null;
|
||||||
@@ -197,6 +288,9 @@ public class AccountWebController {
|
|||||||
case "invalidUsername":
|
case "invalidUsername":
|
||||||
addMessage = "invalidUsernameMessage";
|
addMessage = "invalidUsernameMessage";
|
||||||
break;
|
break;
|
||||||
|
case "invalidPassword":
|
||||||
|
addMessage = "invalidPasswordMessage";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -212,16 +306,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";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +374,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";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 @@ 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;
|
||||||
|
|
||||||
@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) {
|
||||||
@@ -97,7 +102,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();
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
||||||
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "")
|
@ConfigurationProperties(prefix = "")
|
||||||
@@ -128,6 +132,15 @@ public class ApplicationProperties {
|
|||||||
private OAUTH2 oauth2;
|
private OAUTH2 oauth2;
|
||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
|
private String loginMethod = "all";
|
||||||
|
|
||||||
|
public String getLoginMethod() {
|
||||||
|
return loginMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoginMethod(String loginMethod) {
|
||||||
|
this.loginMethod = loginMethod;
|
||||||
|
}
|
||||||
|
|
||||||
public int getLoginAttemptCount() {
|
public int getLoginAttemptCount() {
|
||||||
return loginAttemptCount;
|
return loginAttemptCount;
|
||||||
@@ -187,6 +200,8 @@ public class ApplicationProperties {
|
|||||||
+ initialLogin
|
+ initialLogin
|
||||||
+ ", csrfDisabled="
|
+ ", csrfDisabled="
|
||||||
+ csrfDisabled
|
+ csrfDisabled
|
||||||
|
+ ", loginMethod="
|
||||||
|
+ loginMethod
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,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;
|
||||||
@@ -271,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;
|
||||||
}
|
}
|
||||||
@@ -341,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
|
||||||
+ "]";
|
+ "]";
|
||||||
@@ -355,7 +383,7 @@ public class ApplicationProperties {
|
|||||||
private GithubProvider github = new GithubProvider();
|
private GithubProvider github = new GithubProvider();
|
||||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||||
|
|
||||||
public Provider get(String registrationId) throws Exception {
|
public Provider get(String registrationId) throws UnsupportedProviderException {
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "google":
|
case "google":
|
||||||
return getGoogle();
|
return getGoogle();
|
||||||
@@ -366,7 +394,8 @@ public class ApplicationProperties {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
throw new Exception("Provider not supported, use custom setting.");
|
throw new UnsupportedProviderException(
|
||||||
|
"Logout from the provider is not supported? Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||||
}
|
}
|
||||||
|
|
||||||
public GoogleProvider getGoogle() {
|
public GoogleProvider getGoogle() {
|
||||||
@@ -407,315 +436,21 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GoogleProvider extends Provider {
|
|
||||||
|
|
||||||
private static final String authorizationUri =
|
|
||||||
"https://accounts.google.com/o/oauth2/v2/auth";
|
|
||||||
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
|
|
||||||
private static final String userInfoUri =
|
|
||||||
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
|
|
||||||
|
|
||||||
public String getAuthorizationuri() {
|
|
||||||
return authorizationUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenuri() {
|
|
||||||
return tokenUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserinfouri() {
|
|
||||||
return userInfoUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String clientId;
|
|
||||||
private String clientSecret;
|
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
|
||||||
private String useAsUsername = "email";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientId() {
|
|
||||||
return this.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientId(String clientId) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientSecret() {
|
|
||||||
return this.clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientSecret(String clientSecret) {
|
|
||||||
this.clientSecret = clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> getScopes() {
|
|
||||||
if (scopes == null || scopes.isEmpty()) {
|
|
||||||
scopes = new ArrayList<>();
|
|
||||||
scopes.add("https://www.googleapis.com/auth/userinfo.email");
|
|
||||||
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
|
|
||||||
}
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setScopes(String scopes) {
|
|
||||||
this.scopes =
|
|
||||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUseAsUsername() {
|
|
||||||
return this.useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUseAsUsername(String useAsUsername) {
|
|
||||||
this.useAsUsername = useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Google [clientId="
|
|
||||||
+ clientId
|
|
||||||
+ ", clientSecret="
|
|
||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
|
||||||
+ ", scopes="
|
|
||||||
+ scopes
|
|
||||||
+ ", useAsUsername="
|
|
||||||
+ useAsUsername
|
|
||||||
+ "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "google";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientName() {
|
|
||||||
return "Google";
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSettingsValid() {
|
|
||||||
return super.isValid(this.getClientId(), "clientId")
|
|
||||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
|
||||||
&& super.isValid(this.getScopes(), "scopes")
|
|
||||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GithubProvider extends Provider {
|
|
||||||
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
|
|
||||||
private static final String tokenUri = "https://github.com/login/oauth/access_token";
|
|
||||||
private static final String userInfoUri = "https://api.github.com/user";
|
|
||||||
|
|
||||||
public String getAuthorizationuri() {
|
|
||||||
return authorizationUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTokenuri() {
|
|
||||||
return tokenUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUserinfouri() {
|
|
||||||
return userInfoUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String clientId;
|
|
||||||
private String clientSecret;
|
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
|
||||||
private String useAsUsername = "login";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getIssuer() {
|
|
||||||
return new String();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIssuer(String issuer) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientId() {
|
|
||||||
return this.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientId(String clientId) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientSecret() {
|
|
||||||
return this.clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientSecret(String clientSecret) {
|
|
||||||
this.clientSecret = clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<String> getScopes() {
|
|
||||||
if (scopes == null || scopes.isEmpty()) {
|
|
||||||
scopes = new ArrayList<>();
|
|
||||||
scopes.add("read:user");
|
|
||||||
}
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setScopes(String scopes) {
|
|
||||||
this.scopes =
|
|
||||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUseAsUsername() {
|
|
||||||
return this.useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUseAsUsername(String useAsUsername) {
|
|
||||||
this.useAsUsername = useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "GitHub [clientId="
|
|
||||||
+ clientId
|
|
||||||
+ ", clientSecret="
|
|
||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
|
||||||
+ ", scopes="
|
|
||||||
+ scopes
|
|
||||||
+ ", useAsUsername="
|
|
||||||
+ useAsUsername
|
|
||||||
+ "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "github";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientName() {
|
|
||||||
return "GitHub";
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSettingsValid() {
|
|
||||||
return super.isValid(this.getClientId(), "clientId")
|
|
||||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
|
||||||
&& super.isValid(this.getScopes(), "scopes")
|
|
||||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class KeycloakProvider extends Provider {
|
|
||||||
private String issuer;
|
|
||||||
private String clientId;
|
|
||||||
private String clientSecret;
|
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
|
||||||
private String useAsUsername = "email";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getIssuer() {
|
|
||||||
return this.issuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIssuer(String issuer) {
|
|
||||||
this.issuer = issuer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientId() {
|
|
||||||
return this.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientId(String clientId) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientSecret() {
|
|
||||||
return this.clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setClientSecret(String clientSecret) {
|
|
||||||
this.clientSecret = clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<String> getScopes() {
|
|
||||||
if (scopes == null || scopes.isEmpty()) {
|
|
||||||
scopes = new ArrayList<>();
|
|
||||||
scopes.add("profile");
|
|
||||||
scopes.add("email");
|
|
||||||
}
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setScopes(String scopes) {
|
|
||||||
this.scopes =
|
|
||||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getUseAsUsername() {
|
|
||||||
return this.useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setUseAsUsername(String useAsUsername) {
|
|
||||||
this.useAsUsername = useAsUsername;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "Keycloak [issuer="
|
|
||||||
+ issuer
|
|
||||||
+ ", clientId="
|
|
||||||
+ clientId
|
|
||||||
+ ", clientSecret="
|
|
||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
|
||||||
+ ", scopes="
|
|
||||||
+ scopes
|
|
||||||
+ ", useAsUsername="
|
|
||||||
+ useAsUsername
|
|
||||||
+ "]";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return "keycloak";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getClientName() {
|
|
||||||
return "Keycloak";
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSettingsValid() {
|
|
||||||
return isValid(this.getIssuer(), "issuer")
|
|
||||||
&& isValid(this.getClientId(), "clientId")
|
|
||||||
&& isValid(this.getClientSecret(), "clientSecret")
|
|
||||||
&& isValid(this.getScopes(), "scopes")
|
|
||||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class System {
|
public static class System {
|
||||||
private String defaultLocale;
|
private String defaultLocale;
|
||||||
private Boolean googlevisibility;
|
private Boolean googlevisibility;
|
||||||
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;
|
||||||
|
|||||||