Compare commits
377 Commits
0025d5ad15
...
016d7da0a0
Author | SHA1 | Date | |
---|---|---|---|
|
016d7da0a0 | ||
|
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 | ||
|
7b08d98232 | ||
|
03150c6462 | ||
|
a3bf7baf35 | ||
|
6c09bcf23c | ||
|
e11fa01d10 | ||
|
d60107f48b | ||
|
0b449af9ba | ||
|
4c9c0207ba | ||
|
d36a59442f | ||
|
fd4c75279f | ||
|
fd5f5025ce | ||
|
319ecbcbc1 | ||
|
04b0bcde61 | ||
|
6499b759d9 | ||
|
2081c4872d | ||
|
37c75971f2 | ||
|
7d9edfca6d | ||
|
a40696f16e | ||
|
515b5b1492 | ||
|
5277cf2b59 | ||
|
e824a3e7bd | ||
|
9fd508fcc7 | ||
|
a0227a4bdd | ||
|
87be41117f | ||
|
3e9123fcd5 | ||
|
7e7c6a3832 | ||
|
cce31ee0f6 | ||
|
746f341d6a | ||
|
f35bf120e9 | ||
|
1fd4ce339f | ||
|
af736ca33a | ||
|
0878dd10b8 | ||
|
948ddb06bc | ||
|
efc07522ab | ||
|
31938b662c | ||
|
eb526a5d0c | ||
|
c4a620e3f5 | ||
|
17bf6237a2 | ||
|
086daf8351 | ||
|
2741094a8a | ||
|
b4dc766f7a | ||
|
75e2cfb234 | ||
|
c5f7000e72 | ||
|
ca0432e0b4 | ||
|
5799e61385 | ||
|
f2784c85d6 | ||
|
01a3fa8cfe | ||
|
41138cb2be | ||
|
995de6abc3 | ||
|
36deb32f07 | ||
|
6a38c55867 | ||
|
96e390c98d | ||
|
52978ec9ad | ||
|
fcd4af2d09 | ||
|
963c1f4874 | ||
|
aa42806a9e | ||
|
19c564a6f7 | ||
|
6afbd8bd24 | ||
|
76bfc09a44 | ||
|
6df412c576 | ||
|
37bb890cb9 | ||
|
9bd15d7ef3 | ||
|
b0671943f7 | ||
|
7cbad4df4f | ||
|
e27651826e | ||
|
5b0de9eac1 | ||
|
b646d8c481 | ||
|
cd2f628168 | ||
|
aef0d32b5b | ||
|
e761ad8e51 | ||
|
059296d444 | ||
|
9c1de1cb10 | ||
|
ebba39ce10 | ||
|
361b4c2db4 | ||
|
d221654121 | ||
|
7ac41d7863 | ||
|
f1476d197f | ||
|
4a53195c25 | ||
|
e2bed6f6af | ||
|
5d6e23d4b7 | ||
|
dfb8ba857f | ||
|
1572404e6f | ||
|
76dc90d587 | ||
|
316b4e42af | ||
|
f61bbd312f | ||
|
65b9544942 | ||
|
7e8b86e6eb | ||
|
2ab5bc1e18 | ||
|
01b2613efe | ||
|
50fc13b30c | ||
|
b7dc248f93 | ||
|
8b05204047 | ||
|
18680f2847 | ||
|
6c790299aa | ||
|
65d662588e | ||
|
ab7acb5db3 | ||
|
dde0f5cd10 | ||
|
5d70217961 | ||
|
a0f0a446de | ||
|
52e9689431 | ||
|
cd6f3862f6 | ||
|
0f4eb8398a | ||
|
c9a3f48e5a | ||
|
a7f67961e7 | ||
|
32209534a0 | ||
|
7196f0f970 | ||
|
64d0be5ffa | ||
|
31be5baf3d | ||
|
4781fd515b | ||
|
11497f52d4 | ||
|
fa934f06ab | ||
|
3d78e01559 | ||
|
65f9438639 | ||
|
6ffa80c386 | ||
|
8eb7b18089 | ||
|
9041441c46 | ||
|
502a4b1cc3 | ||
|
ce13648075 | ||
|
9644557a9e | ||
|
01964add79 | ||
|
822e771f45 | ||
|
c0888fb938 | ||
|
5cdb3bee21 | ||
|
4cce6c1c21 | ||
|
b928e294d1 | ||
|
ec3aa17f65 | ||
|
851d77de8e | ||
|
137fdaca6a | ||
|
7371f4e87f | ||
|
6529eb6b61 | ||
|
729af56d1b | ||
|
9f4a600eba | ||
|
ddb2528ecf | ||
|
eb5aeb4595 | ||
|
b93bff5cad | ||
|
3ae891c62e | ||
|
48bd060d6e | ||
|
5dee64ab7b | ||
|
a2f66493ea | ||
|
37a0103699 | ||
|
2dabf8955d | ||
|
9ab471fb63 | ||
|
801fd8bb21 | ||
|
5e9c780d31 | ||
|
bbaaaf7ae6 | ||
|
befd0974f3 | ||
|
250c317155 | ||
|
2c148eb0c0 | ||
|
ead8010bd1 | ||
|
0962159523 | ||
|
cbb4ccd4b7 | ||
|
41e73e4fd1 | ||
|
86a6ea5a26 | ||
|
ce5af5ddde | ||
|
0d193cd235 | ||
|
f06d755899 | ||
|
4dcf2f5870 | ||
|
c2179ccd63 | ||
|
17ef2e9b5d | ||
|
94445bceb1 | ||
|
801dcdb463 | ||
|
7b49d85804 | ||
|
4190aa20a6 | ||
|
5a832198b4 | ||
|
2066bb2ae8 | ||
|
5d64c97406 | ||
|
d648c6d4b4 | ||
|
435bfa3b3f | ||
|
4d0135d7b7 | ||
|
5975928e89 | ||
|
0f8d2937eb | ||
|
53fcc51541 | ||
|
23b60c73a0 | ||
|
65321991c6 | ||
|
9d56014ca0 | ||
|
7b2493a838 | ||
|
1d4db6493d | ||
|
c4bfb44f72 | ||
|
d9e7ae1380 | ||
|
47b10d45d2 | ||
|
ffd27acedc | ||
|
841b8a6439 | ||
|
611d2b22d2 | ||
|
4bad105119 | ||
|
c7b3f89f48 | ||
|
1dd0852e54 | ||
|
c1bb1002f5 | ||
|
367146b9ad | ||
|
1f1cdf6fe8 | ||
|
84b355951f | ||
|
bfb82e38ab | ||
|
149249c6ac | ||
|
4232f359c7 | ||
|
6adeecac9c | ||
|
0cc12f4456 | ||
|
2646af19b3 | ||
|
ea02369c76 | ||
|
e48c125f2c | ||
|
46c9055e11 | ||
|
d9206df038 | ||
|
5dee084ad6 | ||
|
2319ab3d9e | ||
|
7fbb94f2bd | ||
|
4167e13f76 | ||
|
1609173907 | ||
|
d142f0abd6 | ||
|
c2949d8944 | ||
|
4b2d02ee14 | ||
|
4766201621 | ||
|
d9043c9100 | ||
|
d52b0d0082 | ||
|
ecfbaef933 | ||
|
c5e6555bb5 | ||
|
81d7cc0a40 | ||
|
02524c64d5 | ||
|
7d6846920e | ||
|
2443ed2020 | ||
|
0dd91f209a | ||
|
869e2dc62d | ||
|
a28f14b70e | ||
|
b5de6a73cc | ||
|
45e2623b9b | ||
|
6d95bfdee0 | ||
|
f9111e556c | ||
|
fa746a2b51 | ||
|
e43292f4dd | ||
|
c56c5f80ab | ||
|
f2eb5dd7d3 | ||
|
ffe221b93c | ||
|
3f252e29a1 | ||
|
7c0fd02126 | ||
|
7109dd7905 | ||
|
e30665e7c8 | ||
|
514606789e | ||
|
dd1a441f92 | ||
|
31c48aec90 | ||
|
6709e0c46d | ||
|
10dd5e4a40 | ||
|
6fc9a2032a | ||
|
ffec5f7b54 | ||
|
b904a46bca | ||
|
26a457f9d0 | ||
|
e9042e0b7e | ||
|
521dff737f | ||
|
2968a696cd | ||
|
e0d3bbf13b | ||
|
89e763d959 | ||
|
1f9a0ed0e3 | ||
|
f203e07f55 | ||
|
56d4c02445 | ||
|
c20936b485 | ||
|
389323c190 | ||
|
f0dd48b3b1 | ||
|
b860146c93 | ||
|
23b662e5dc | ||
|
7160ba47b1 | ||
|
bdea990b18 | ||
|
1cbc6307c3 | ||
|
1e42f54ec7 | ||
|
44e85a1d38 | ||
|
54073767af | ||
|
3fa41e058a | ||
|
44f9313012 | ||
|
ce3e98e240 | ||
|
36192ba560 | ||
|
0e262dc2bd | ||
|
dcf13e9ade | ||
|
811c19e00d | ||
|
f2b7aeeb1c | ||
|
840694c527 | ||
|
21e5002d73 | ||
|
36d6c06237 | ||
|
c1fea7c92f | ||
|
29ec42bc35 | ||
|
425502b3e3 | ||
|
692a526900 | ||
|
3a27d97811 | ||
|
af91c73e7a | ||
|
526a30d033 | ||
|
bf8d6d2337 | ||
|
5628300f51 | ||
|
72ba97a00c | ||
|
1634987171 | ||
|
0f43723250 | ||
|
34e2128a39 | ||
|
0a0887aafc | ||
|
7c0c33ca63 | ||
|
8b2f24affd | ||
|
cbe750c76c | ||
|
5f6d24f805 | ||
|
be5d5fdf04 | ||
|
a04dc605df | ||
|
503acc9408 | ||
|
9b166da57d | ||
|
66e566555e | ||
|
7ba0067688 | ||
|
f7aebf22c8 | ||
|
bb0b5f0528 | ||
|
9e3d5a5bc5 | ||
|
9ba5c6d4be | ||
|
00f7fe7ac3 | ||
|
f4fcede771 | ||
|
b69646d00b | ||
|
9e81b161c3 | ||
|
66ce7511ca | ||
|
d17db24aa9 | ||
|
ac5273244c | ||
|
27113f99cb | ||
|
fa31a4e340 | ||
|
4d53119390 | ||
|
303b8e032b | ||
|
d6b1fec69d | ||
|
5c572a7d89 | ||
|
04d1ff3822 | ||
|
eb8a494b5c | ||
|
4dfac2f46f | ||
|
547f231e29 | ||
|
38979dd362 | ||
|
890163053b | ||
|
fbbc71d7e6 | ||
|
4372536c17 | ||
|
7f577a6052 | ||
|
d7afc574a6 | ||
|
caa5525ddd | ||
|
c622ee915b | ||
|
d0df392eef | ||
|
1c33500815 | ||
|
d730c6a12f | ||
|
b71f6f93b1 | ||
|
32dd328048 | ||
|
9402109663 | ||
|
a05cfd52cb | ||
|
ad0967f7d0 | ||
|
7e4d8f45f6 | ||
|
9dbc2712e7 |
2
.github/FUNDING.yml
vendored
@ -10,4 +10,4 @@ liberapay: # Replace with a single Liberapay username
|
|||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
otechie: # Replace with a single Otechie username
|
otechie: # Replace with a single Otechie username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
custom: ['https://paypal.me/froodleplex?country.x=GB&locale.x=en_GB'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
|
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
|
76
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Submit a new feature request.
|
||||||
|
title: "[Feature Request]: "
|
||||||
|
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
|
8
.github/workflows/build.yml
vendored
@ -3,14 +3,8 @@ name: "Build repo"
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths-ignore:
|
|
||||||
- ".github/**"
|
|
||||||
- "**/*.md"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths-ignore:
|
|
||||||
- ".github/**"
|
|
||||||
- "**/*.md"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -36,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 7.6
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --no-build-cache
|
run: ./gradlew build --no-build-cache
|
||||||
|
30
.github/workflows/push-docker.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 7.6
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean build
|
run: ./gradlew clean build
|
||||||
@ -110,3 +110,31 @@ jobs:
|
|||||||
labels: ${{ steps.meta2.outputs.labels }}
|
labels: ${{ steps.meta2.outputs.labels }}
|
||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
|
- name: Generate tags fat
|
||||||
|
id: meta3
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
|
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
|
- name: Build and push main Dockerfile fat
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
if: github.ref != 'refs/heads/main'
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile-fat
|
||||||
|
push: true
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
tags: ${{ steps.meta3.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta3.outputs.labels }}
|
||||||
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
2
.github/workflows/releaseArtifacts.yml
vendored
@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 7.6
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||||
run: ./gradlew clean createExe
|
run: ./gradlew clean createExe
|
||||||
|
9
.github/workflows/test.yml
vendored
@ -32,6 +32,15 @@ jobs:
|
|||||||
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 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
|
# 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
|
- name: Run Docker Compose Tests
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./test.sh
|
chmod +x ./test.sh
|
||||||
|
3
.gitignore
vendored
@ -125,3 +125,6 @@ watchedFolders/
|
|||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
|
#cucumber
|
||||||
|
/cucumber/reports/**
|
@ -29,7 +29,7 @@ If you would like to add or modify a translation, please see [How to add new lan
|
|||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Documentation for Stirling-PDF is handled in a seperate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
|
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
|
||||||
|
|
||||||
## Fixing Bugs or Adding a New Feature
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
|
23
Dockerfile
@ -1,16 +1,15 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:20240329
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
COPY pipeline /pipeline
|
COPY pipeline /pipeline
|
||||||
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|
||||||
# Set Environment Variables
|
# Set Environment Variables
|
||||||
ENV DOCKER_ENABLE_SECURITY=false \
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
VERSION_TAG=$VERSION_TAG \
|
VERSION_TAG=$VERSION_TAG \
|
||||||
@ -20,25 +19,24 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022
|
UMASK=022
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
apk update && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini \
|
tini \
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
openjdk17-jre \
|
|
||||||
su-exec \
|
|
||||||
shadow \
|
shadow \
|
||||||
|
su-exec \
|
||||||
|
openssl \
|
||||||
|
openssl-dev \
|
||||||
|
openjdk21-jre \
|
||||||
# Doc conversion
|
# Doc conversion
|
||||||
libreoffice@testing \
|
libreoffice \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
@ -60,10 +58,9 @@ openssl-dev \
|
|||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
tesseract --list-langs && \
|
tesseract --list-langs
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
84
Dockerfile-fat
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
# Build the application
|
||||||
|
FROM gradle:7.6-jdk17 AS build
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy the entire project to the working directory
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application with DOCKER_ENABLE_SECURITY=false
|
||||||
|
RUN DOCKER_ENABLE_SECURITY=true \
|
||||||
|
./gradlew clean build
|
||||||
|
|
||||||
|
# Main stage
|
||||||
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
|
# Copy necessary files
|
||||||
|
COPY scripts /scripts
|
||||||
|
COPY pipeline /pipeline
|
||||||
|
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
|
||||||
|
COPY --from=build /app/build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
# Set Environment Variables
|
||||||
|
ENV DOCKER_ENABLE_SECURITY=false \
|
||||||
|
VERSION_TAG=$VERSION_TAG \
|
||||||
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
||||||
|
HOME=/home/stirlingpdfuser \
|
||||||
|
PUID=1000 \
|
||||||
|
PGID=1000 \
|
||||||
|
UMASK=022 \
|
||||||
|
FAT_DOCKER=true \
|
||||||
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=true
|
||||||
|
|
||||||
|
|
||||||
|
# JDK for app
|
||||||
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk upgrade --no-cache -a && \
|
||||||
|
apk add --no-cache \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata \
|
||||||
|
tini \
|
||||||
|
bash \
|
||||||
|
curl \
|
||||||
|
calibre@testing \
|
||||||
|
shadow \
|
||||||
|
su-exec \
|
||||||
|
openssl \
|
||||||
|
openssl-dev \
|
||||||
|
openjdk21-jre \
|
||||||
|
# Doc conversion
|
||||||
|
libreoffice \
|
||||||
|
# pdftohtml
|
||||||
|
poppler-utils \
|
||||||
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
|
ocrmypdf \
|
||||||
|
tesseract-ocr-data-eng \
|
||||||
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||||
|
# CV
|
||||||
|
py3-opencv \
|
||||||
|
# python3/pip
|
||||||
|
python3 && \
|
||||||
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
|
# uno unoconv and HTML
|
||||||
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
|
fc-cache -f -v && \
|
||||||
|
chmod +x /scripts/* && \
|
||||||
|
chmod +x /scripts/init.sh && \
|
||||||
|
# User permissions
|
||||||
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
|
||||||
|
tesseract --list-langs
|
||||||
|
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
|
# Set user and run command
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.19.1
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
@ -18,24 +18,23 @@ COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
|||||||
COPY pipeline /pipeline
|
COPY pipeline /pipeline
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
|
|
||||||
# Set up necessary directories and permissions
|
# Set up necessary directories and permissions
|
||||||
|
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
||||||
RUN mkdir /configs /logs /customFiles && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
||||||
chmod +x /scripts/*.sh && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
su-exec \
|
|
||||||
shadow \
|
shadow \
|
||||||
openjdk17-jre && \
|
su-exec \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
|
openjdk21-jre && \
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
|
|
||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
|
||||||
# User permissions
|
# User permissions
|
||||||
|
mkdir /configs /logs /customFiles && \
|
||||||
|
chmod +x /scripts/*.sh && \
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
@ -43,9 +42,8 @@ RUN mkdir /configs /logs /customFiles && \
|
|||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080/tcp
|
||||||
|
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
|
||||||
|
|
||||||
# Run the application
|
# Run the application
|
||||||
|
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
|
||||||
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|
||||||
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
|
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- |
|
||||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| crop | ✔️ | | | | | | | | | ✔️ | |
|
| crop | ✔️ | | | | | | | | | ✔️ | |
|
||||||
@ -26,6 +26,7 @@
|
|||||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||||
|
@ -14,7 +14,7 @@ You could theoretically use a Distrobox/Toolbox, if your Distribution has old or
|
|||||||
|
|
||||||
Install the following software, if not already installed:
|
Install the following software, if not already installed:
|
||||||
|
|
||||||
- Java 17 or later
|
- Java 17 or later (21 recommended)
|
||||||
|
|
||||||
- Gradle 7.0 or later (included within repo so not needed on server)
|
- Gradle 7.0 or later (included within repo so not needed on server)
|
||||||
|
|
||||||
@ -42,17 +42,25 @@ For Debian-based systems, you can use the following command:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-17-jdk python3 python3-pip
|
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora-based systems use this command:
|
For Fedora-based systems use this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-17-openjdk python3 python3-pip
|
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip
|
||||||
|
```
|
||||||
|
|
||||||
|
For non-root users with Nix Package Manager, use the following command:
|
||||||
|
```bash
|
||||||
|
nix-channel --update
|
||||||
|
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
|
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
|
||||||
|
|
||||||
|
For Debian and Fedora, you can build it from source using the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir ~/.git
|
mkdir ~/.git
|
||||||
cd ~/.git &&\
|
cd ~/.git &&\
|
||||||
@ -64,6 +72,11 @@ make &&\
|
|||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.jbig2enc
|
||||||
|
```
|
||||||
|
|
||||||
### Step 3: Install Additional Software
|
### Step 3: Install Additional Software
|
||||||
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality.
|
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality.
|
||||||
|
|
||||||
@ -105,6 +118,13 @@ sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpa
|
|||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
|
||||||
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
|
```
|
||||||
|
|
||||||
### Step 4: Clone and Build Stirling-PDF
|
### Step 4: Clone and Build Stirling-PDF
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -115,13 +135,12 @@ chmod +x ./gradlew &&\
|
|||||||
./gradlew build
|
./gradlew build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Step 5: Move jar to desired location
|
### Step 5: Move jar to desired location
|
||||||
|
|
||||||
After the build process, a `.jar` file will be generated in the `build/libs` directory.
|
After the build process, a `.jar` file will be generated in the `build/libs` directory.
|
||||||
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
|
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
|
||||||
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
|
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
|
||||||
This folder is required for the python scripts using OpenCV
|
This folder is required for the python scripts using OpenCV.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir /opt/Stirling-PDF &&\
|
sudo mkdir /opt/Stirling-PDF &&\
|
||||||
@ -129,19 +148,25 @@ sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
|
|||||||
sudo mv scripts /opt/Stirling-PDF/ &&\
|
sudo mv scripts /opt/Stirling-PDF/ &&\
|
||||||
echo "Scripts installed."
|
echo "Scripts installed."
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
|
||||||
|
```bash
|
||||||
|
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
|
||||||
|
```
|
||||||
|
|
||||||
### Step 6: Other files
|
### Step 6: Other files
|
||||||
#### OCR
|
#### OCR
|
||||||
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
|
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
|
||||||
|
|
||||||
##### Installing Language Packs
|
##### Installing Language Packs
|
||||||
Easiest is to use the langpacks provided by your repositories. Skip the other steps
|
Easiest is to use the langpacks provided by your repositories. Skip the other steps.
|
||||||
|
|
||||||
Manual:
|
Manual:
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
||||||
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||||
3.
|
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
||||||
Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
|
||||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
||||||
|
|
||||||
Debian based systems, install languages with this command:
|
Debian based systems, install languages with this command:
|
||||||
@ -171,14 +196,38 @@ dnf search -C tesseract-langpack-
|
|||||||
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Nix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-env -iA nixpkgs.tesseract
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Nix Package Manager pre-installs almost all the language packs when tesseract is installed.
|
||||||
|
|
||||||
### Step 7: Run Stirling-PDF
|
### Step 7: Run Stirling-PDF
|
||||||
|
|
||||||
|
Those who have pushed to the root directory, run the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew bootRun
|
./gradlew bootRun
|
||||||
or
|
or
|
||||||
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Since libreoffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
|
||||||
|
```
|
||||||
|
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory ‘/run/user/1501’: Permission denied
|
||||||
|
```
|
||||||
|
To resolve this, before starting the Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir temp
|
||||||
|
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
|
||||||
|
./gradlew bootRun
|
||||||
|
or
|
||||||
|
java -jar ./Stirling-PDF-*.jar
|
||||||
|
```
|
||||||
|
|
||||||
### Step 8: Adding a Desktop icon
|
### Step 8: Adding a Desktop icon
|
||||||
|
|
||||||
This will add a modified Appstarter to your Appmenu.
|
This will add a modified Appstarter to your Appmenu.
|
||||||
@ -202,7 +251,19 @@ EOF
|
|||||||
|
|
||||||
Note: Currently the app will run in the background until manually closed.
|
Note: Currently the app will run in the background until manually closed.
|
||||||
|
|
||||||
### Optional: Run Stirling-PDF as a service
|
### Optional: Changing the host and port of the application:
|
||||||
|
|
||||||
|
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
server:
|
||||||
|
host: 0.0.0.0
|
||||||
|
port: 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
|
||||||
|
|
||||||
|
### Optional: Run Stirling-PDF as a service (requires root).
|
||||||
|
|
||||||
First create a .env file, where you can store environment variables:
|
First create a .env file, where you can store environment variables:
|
||||||
```
|
```
|
||||||
@ -239,6 +300,7 @@ WantedBy=multi-user.target
|
|||||||
```
|
```
|
||||||
|
|
||||||
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
```
|
```
|
||||||
|
109
README.md
@ -5,12 +5,12 @@
|
|||||||
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
|
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
|
||||||
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
|
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
|
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL)
|
||||||
[![Github Sponsor](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
|
[![Github Sponsor](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
|
||||||
|
|
||||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
|
|
||||||
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. Originally developed entirely by ChatGPT, 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.
|
||||||
|
|
||||||
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
||||||
|
|
||||||
@ -159,37 +159,41 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
|
|||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling PDF currently supports 27!
|
Stirling PDF currently supports 32!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| ------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
|
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
|
||||||
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
|
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
|
||||||
| Arabic (العربية) (ar_AR) | ![42%](https://geps.dev/progress/42) |
|
| Arabic (العربية) (ar_AR) | ![40%](https://geps.dev/progress/40) |
|
||||||
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
|
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
|
||||||
| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) |
|
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Spanish (Español) (es_ES) | ![99%](https://geps.dev/progress/99) |
|
| Spanish (Español) (es_ES) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) | ![98%](https://geps.dev/progress/98) |
|
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
|
| Traditional Chinese (繁體中文) (zh_TW) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Catalan (Català) (ca_CA) | ![51%](https://geps.dev/progress/51) |
|
| Catalan (Català) (ca_CA) | ![49%](https://geps.dev/progress/49) |
|
||||||
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
|
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
|
||||||
| Swedish (Svenska) (sv_SE) | ![42%](https://geps.dev/progress/42) |
|
| Swedish (Svenska) (sv_SE) | ![40%](https://geps.dev/progress/40) |
|
||||||
| Polish (Polski) (pl_PL) | ![44%](https://geps.dev/progress/44) |
|
| Polish (Polski) (pl_PL) | ![42%](https://geps.dev/progress/42) |
|
||||||
| Romanian (Română) (ro_RO) | ![41%](https://geps.dev/progress/41) |
|
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) |
|
||||||
| Korean (한국어) (ko_KR) | ![91%](https://geps.dev/progress/91) |
|
| Korean (한국어) (ko_KR) | ![87%](https://geps.dev/progress/87) |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) | ![63%](https://geps.dev/progress/63) |
|
| Portuguese Brazilian (Português) (pt_BR) | ![61%](https://geps.dev/progress/61) |
|
||||||
| Russian (Русский) (ru_RU) | ![91%](https://geps.dev/progress/91) |
|
| Russian (Русский) (ru_RU) | ![86%](https://geps.dev/progress/86) |
|
||||||
| Basque (Euskara) (eu_ES) | ![66%](https://geps.dev/progress/66) |
|
| Basque (Euskara) (eu_ES) | ![63%](https://geps.dev/progress/63) |
|
||||||
| Japanese (日本語) (ja_JP) | ![91%](https://geps.dev/progress/91) |
|
| Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) |
|
||||||
| Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) |
|
| Dutch (Nederlands) (nl_NL) | ![84%](https://geps.dev/progress/84) |
|
||||||
| Greek (Ελληνικά) (el_GR) | ![88%](https://geps.dev/progress/88) |
|
| Greek (Ελληνικά) (el_GR) | ![85%](https://geps.dev/progress/85) |
|
||||||
| Turkish (Türkçe) (tr_TR) | ![99%](https://geps.dev/progress/99) |
|
| Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Indonesia (Bahasa Indonesia) (id_ID) | ![82%](https://geps.dev/progress/82) |
|
| Indonesia (Bahasa Indonesia) (id_ID) | ![78%](https://geps.dev/progress/78) |
|
||||||
| Hindi (हिंदी) (hi_IN) | ![82%](https://geps.dev/progress/82) |
|
| Hindi (हिंदी) (hi_IN) | ![79%](https://geps.dev/progress/79) |
|
||||||
| Hungarian (Magyar) (hu_HU) | ![81%](https://geps.dev/progress/81) |
|
| Hungarian (Magyar) (hu_HU) | ![77%](https://geps.dev/progress/77) |
|
||||||
| Bulgarian (Български) (bg_BG) | ![75%](https://geps.dev/progress/75) |
|
| Bulgarian (Български) (bg_BG) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![84%](https://geps.dev/progress/84) |
|
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![80%](https://geps.dev/progress/80) |
|
||||||
| Ukrainian (Українська) (uk_UA) | ![90%](https://geps.dev/progress/90) |
|
| Ukrainian (Українська) (uk_UA) | ![86%](https://geps.dev/progress/86) |
|
||||||
|
| Slovakian (Slovensky) (sk_SK) | ![94%](https://geps.dev/progress/94) |
|
||||||
|
| Czech (Česky) (cs_CZ) | ![93%](https://geps.dev/progress/93) |
|
||||||
|
| Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) |
|
||||||
|
| Norwegian (Norsk) (no_NB) | ![98%](https://geps.dev/progress/98) |
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
@ -211,39 +215,72 @@ For example in the settings.yml you have
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
system:
|
system:
|
||||||
defaultLocale: 'en-US'
|
enableLogin: 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
|
To have this via an environment variable you would have ``SYSTEM_ENABLELOGIN``
|
||||||
|
|
||||||
The Current list of settings is
|
The Current list of settings is
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true
|
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
|
loginAttemptCount: 5 # lock user account after 5 tries
|
||||||
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
|
# initialLogin:
|
||||||
|
# username: "admin" # Initial username for the first login
|
||||||
|
# password: "stirling" # Initial password for the first login
|
||||||
|
# oauth2:
|
||||||
|
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
|
# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
|
# clientId: "" # Client ID from your provider
|
||||||
|
# clientSecret: "" # Client Secret from your provider
|
||||||
|
# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
|
# 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'
|
||||||
|
# client:
|
||||||
|
# google:
|
||||||
|
# clientId: "" # Client ID for Google OAuth2
|
||||||
|
# clientSecret: "" # Client Secret for Google OAuth2
|
||||||
|
# scopes: "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile" # Scopes for Google OAuth2
|
||||||
|
# useAsUsername: "email" # Field to use as the username for Google OAuth2
|
||||||
|
# github:
|
||||||
|
# clientId: "" # Client ID for GitHub OAuth2
|
||||||
|
# clientSecret: "" # Client Secret for GitHub OAuth2
|
||||||
|
# scopes: "read:user" # Scope for GitHub OAuth2
|
||||||
|
# useAsUsername: "login" # Field to use as the username for GitHub OAuth2
|
||||||
|
# keycloak:
|
||||||
|
# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
||||||
|
# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2
|
||||||
|
# clientSecret: "" # Client Secret for Keycloak OAuth2
|
||||||
|
# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2
|
||||||
|
# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2
|
||||||
|
|
||||||
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)
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
|
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
|
||||||
showUpdate: true # see when a new update is available
|
showUpdate: true # see when a new update is available
|
||||||
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
||||||
|
|
||||||
#ui:
|
ui:
|
||||||
# appName: exampleAppName # Application's visible name
|
appName: null # Application's visible name
|
||||||
# homeDescription: I am a description # Short description or tagline shown on homepage.
|
homeDescription: null # Short description or tagline shown on homepage.
|
||||||
# appNameNavbar: navbarName # Name displayed on the navigation bar
|
appNameNavbar: null # 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'])
|
||||||
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
|
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
|
||||||
|
|
||||||
### Extra notes
|
### Extra notes
|
||||||
|
|
||||||
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
@ -269,7 +306,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 uses 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.
|
||||||
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
|
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
|
||||||
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
|
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
| Technology | Ultra-Lite | Full |
|
| Technology | Ultra-Lite | Full |
|
||||||
|----------------|:----------:|:----:|
|
| ---------- | :--------: | :---: |
|
||||||
| Java | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ |
|
||||||
| Libre | | ✔️ |
|
| Libre | | ✔️ |
|
||||||
@ -7,46 +7,47 @@
|
|||||||
| OpenCV | | ✔️ |
|
| OpenCV | | ✔️ |
|
||||||
| OCRmyPDF | | ✔️ |
|
| OCRmyPDF | | ✔️ |
|
||||||
|
|
||||||
Operation | Ultra-Lite | Full
|
| Operation | Ultra-Lite | Full |
|
||||||
-------------------------|------------|-----
|
| ---------------------- | ---------- | ---- |
|
||||||
add-page-numbers | ✔️ | ✔️
|
| add-page-numbers | ✔️ | ✔️ |
|
||||||
add-password | ✔️ | ✔️
|
| add-password | ✔️ | ✔️ |
|
||||||
add-image | ✔️ | ✔️
|
| add-image | ✔️ | ✔️ |
|
||||||
add-watermark | ✔️ | ✔️
|
| add-watermark | ✔️ | ✔️ |
|
||||||
adjust-contrast | ✔️ | ✔️
|
| adjust-contrast | ✔️ | ✔️ |
|
||||||
auto-split-pdf | ✔️ | ✔️
|
| auto-split-pdf | ✔️ | ✔️ |
|
||||||
auto-redact | ✔️ | ✔️
|
| auto-redact | ✔️ | ✔️ |
|
||||||
auto-rename | ✔️ | ✔️
|
| auto-rename | ✔️ | ✔️ |
|
||||||
cert-sign | ✔️ | ✔️
|
| cert-sign | ✔️ | ✔️ |
|
||||||
crop | ✔️ | ✔️
|
| remove-cert-sign | ✔️ | ✔️ |
|
||||||
change-metadata | ✔️ | ✔️
|
| crop | ✔️ | ✔️ |
|
||||||
change-permissions | ✔️ | ✔️
|
| change-metadata | ✔️ | ✔️ |
|
||||||
compare | ✔️ | ✔️
|
| change-permissions | ✔️ | ✔️ |
|
||||||
extract-page | ✔️ | ✔️
|
| compare | ✔️ | ✔️ |
|
||||||
extract-images | ✔️ | ✔️
|
| extract-page | ✔️ | ✔️ |
|
||||||
flatten | ✔️ | ✔️
|
| extract-images | ✔️ | ✔️ |
|
||||||
get-info-on-pdf | ✔️ | ✔️
|
| flatten | ✔️ | ✔️ |
|
||||||
img-to-pdf | ✔️ | ✔️
|
| get-info-on-pdf | ✔️ | ✔️ |
|
||||||
markdown-to-pdf | ✔️ | ✔️
|
| img-to-pdf | ✔️ | ✔️ |
|
||||||
merge-pdfs | ✔️ | ✔️
|
| markdown-to-pdf | ✔️ | ✔️ |
|
||||||
multi-page-layout | ✔️ | ✔️
|
| merge-pdfs | ✔️ | ✔️ |
|
||||||
overlay-pdf | ✔️ | ✔️
|
| multi-page-layout | ✔️ | ✔️ |
|
||||||
pdf-organizer | ✔️ | ✔️
|
| overlay-pdf | ✔️ | ✔️ |
|
||||||
pdf-to-csv | ✔️ | ✔️
|
| pdf-organizer | ✔️ | ✔️ |
|
||||||
pdf-to-img | ✔️ | ✔️
|
| pdf-to-csv | ✔️ | ✔️ |
|
||||||
pdf-to-single-page | ✔️ | ✔️
|
| pdf-to-img | ✔️ | ✔️ |
|
||||||
remove-pages | ✔️ | ✔️
|
| pdf-to-single-page | ✔️ | ✔️ |
|
||||||
remove-password | ✔️ | ✔️
|
| remove-pages | ✔️ | ✔️ |
|
||||||
rotate-pdf | ✔️ | ✔️
|
| remove-password | ✔️ | ✔️ |
|
||||||
sanitize-pdf | ✔️ | ✔️
|
| rotate-pdf | ✔️ | ✔️ |
|
||||||
scale-pages | ✔️ | ✔️
|
| sanitize-pdf | ✔️ | ✔️ |
|
||||||
sign | ✔️ | ✔️
|
| scale-pages | ✔️ | ✔️ |
|
||||||
show-javascript | ✔️ | ✔️
|
| sign | ✔️ | ✔️ |
|
||||||
split-by-size-or-count | ✔️ | ✔️
|
| show-javascript | ✔️ | ✔️ |
|
||||||
split-pdf-by-sections | ✔️ | ✔️
|
| split-by-size-or-count | ✔️ | ✔️ |
|
||||||
split-pdfs | ✔️ | ✔️
|
| split-pdf-by-sections | ✔️ | ✔️ |
|
||||||
compress-pdf | | ✔️
|
| split-pdfs | ✔️ | ✔️ |
|
||||||
extract-image-scans | | ✔️
|
| compress-pdf | | ✔️ |
|
||||||
ocr-pdf | | ✔️
|
| extract-image-scans | | ✔️ |
|
||||||
pdf-to-pdfa | | ✔️
|
| ocr-pdf | | ✔️ |
|
||||||
remove-blanks | | ✔️
|
| pdf-to-pdfa | | ✔️ |
|
||||||
|
| remove-blanks | | ✔️ |
|
||||||
|
25
build.gradle
@ -12,14 +12,15 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.23.1'
|
version = '0.25.3'
|
||||||
|
|
||||||
|
//17 is lowest but we support and recommend 21
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
renderers = [new JsonReportRenderer()]
|
renderers = [new JsonReportRenderer()]
|
||||||
}
|
}
|
||||||
@ -54,8 +55,8 @@ launch4j {
|
|||||||
headerType="console"
|
headerType="console"
|
||||||
jarTask = tasks.bootJar
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 17?"
|
errTitle="Encountered error, Do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_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"
|
||||||
|
|
||||||
@ -64,8 +65,8 @@ launch4j {
|
|||||||
|
|
||||||
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 17."
|
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21."
|
||||||
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 17."
|
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21."
|
||||||
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
messagesInstanceAlreadyExists="Stirling-PDF is already running."
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +93,13 @@ dependencies {
|
|||||||
implementation("io.github.pixee:java-security-toolkit:1.1.3")
|
implementation("io.github.pixee:java-security-toolkit:1.1.3")
|
||||||
|
|
||||||
implementation 'org.yaml:snakeyaml:2.2'
|
implementation 'org.yaml:snakeyaml:2.2'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.4'
|
|
||||||
|
// Exclude Tomcat and include Jetty
|
||||||
|
implementation('org.springframework.boot:spring-boot-starter-web:3.2.4') {
|
||||||
|
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-thymeleaf:3.2.4'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.4'
|
||||||
|
|
||||||
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
|
||||||
@ -164,7 +171,7 @@ dependencies {
|
|||||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
dependsOn 'spotlessApply'
|
dependsOn 'spotlessApply'
|
||||||
}
|
}
|
||||||
compileJava {
|
compileJava {
|
||||||
@ -188,8 +195,6 @@ swaggerhubUpload {
|
|||||||
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 {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.23.1
|
appVersion: 0.25.3
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
BIN
cucumber/exampleFiles/example.docx
Normal file
BIN
cucumber/exampleFiles/example.odp
Normal file
BIN
cucumber/exampleFiles/example.odt
Normal file
BIN
cucumber/exampleFiles/example.pptx
Normal file
158
cucumber/exampleFiles/example.rtf
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
{\rtf1\ansi\ansicpg1252\uc0\stshfdbch0\stshfloch0\stshfhich0\stshfbi0\deff0\adeff0{\fonttbl{\f0\froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\froman\fcharset2\fprq2{\*\panose 05050102010706020507}Symbol;}{\f2\fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}}{\colortbl;\red0\green0\blue0;\red67\green67\blue67;
|
||||||
|
\red102\green102\blue102;}{\stylesheet{\s0\snext0\sqformat\spriority0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1 Normal;}{\s1\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb400\sa120\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs40\ltrch\b0\i0\fs40\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1 heading 1;}{\s2\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb360\sa120\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs32\ltrch\b0\i0\fs32\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1 heading 2;}{\s3\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb320\sa80\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs28\ltrch\b0\i0\fs28\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf2 heading 3;}{\s4\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb280\sa80\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs24\ltrch\b0\i0\fs24\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf3 heading 4;}{\s5\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb240\sa80\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf3 heading 5;}{\s6\sbasedon0\snext0\styrsid15694742
|
||||||
|
\sqformat\spriority0\keep\keepn\fi0\sb240\sa80\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai\af2\afs22\ltrch\b0\i\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf3 heading 6;}{\*\cs10\additive\ssemihidden\spriority0 Default Paragraph Font;
|
||||||
|
}{\*\ts11\tsrowd\snext11\ssemihidden\spriority0\aspalpha\aspnum\adjustright\ltrpar\li0\lin0\ri0\rin0\ql\faauto\tsvertalt\tsbrdrl\tsbrdrr\tsbrdrt\tsbrdrb\tsbrdrdgr\tsbrdrdgl\tsbrdrh\tsbrdrv\trpaddl108\trpaddfl3\trwWidthB0\trftsWidthB3\trpaddt0\trpaddft3\trpaddb0
|
||||||
|
\trpaddfb3\trpaddr108\trpaddfr3 Normal Table;}{\s15\sbasedon0\snext15\styrsid15694742\sqformat\spriority0\keep\keepn\fi0\sb0\sa60\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs52\ltrch\b0\i0\fs52\loch\af2
|
||||||
|
\dbch\af2\hich\f2\strike0\ulnone\cf1 Title;}{\s16\sbasedon0\snext16\styrsid15694742\sqformat\spriority0\keep\keepn\fi0\sb0\sa320\aspalpha\aspnum\adjustright\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl240\slmult1\rtlch\ab0\ai0\af2\afs30\ltrch\b0\i0\fs30
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf3 Subtitle;}}{\*\rsidtbl\rsid10976062\rsid13249109}{\*\generator Aspose.Words for Java 23.4.0;}{\info\version1\edmins0\nofpages1\nofwords0\nofchars0\nofcharsws0}\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0{
|
||||||
|
\mmathPr\mbrkBin0\mbrkBinSub0\mdefJc1\mdispDef1\minterSp0\mintLim0\mintraSp0\mlMargin0\mmathFont0\mnaryLim1\mpostSp0\mpreSp0\mrMargin0\msmallFrac0\mwrapIndent1440\mwrapRight0}\deflang1033\deflangfe2052\adeflang1025\jexpand\showxmlerrors1\validatexml1{
|
||||||
|
\*\wgrffmtfilter 013f}\viewkind1\viewscale100\fet0\ftnbj\aenddoc\ftnrstcont\aftnrstcont\ftnnar\aftnnrlc\widowctrl\nospaceforul\nolnhtadjtbl\alntblind\lyttblrtgr\dntblnsbdb\noxlattoyen\wrppunct\nobrkwrptbl\expshrtn\snaptogridincell\asianbrkrule\htmautsp\noultrlspc
|
||||||
|
\useltbaln\splytwnine\ftnlytwnine\lytcalctblwd\allowfieldendsel\lnbrkrule\nouicompat\nofeaturethrottle1\utinl\formshade\nojkernpunct\dghspace180\dgvspace180\dghorigin1800\dgvorigin1440\dghshow1\dgvshow1\dgmargin\pgbrdrhead\pgbrdrfoot\rsidroot10976062\sectd\sectlinegrid360\pgwsxn12240\pghsxn15840\marglsxn1440\margrsxn1440\margtsxn1440\margbsxn1440\guttersxn0\headery720\footery720\colsx720\ltrsect\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar
|
||||||
|
\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af2\dbch\af2
|
||||||
|
\hich\f2\strike0\ulnone\cf1 A}{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar
|
||||||
|
\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033\loch\af2
|
||||||
|
\dbch\af2\hich\f2\strike0\ulnone\cf1 B}{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar
|
||||||
|
\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard
|
||||||
|
\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0
|
||||||
|
\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0
|
||||||
|
\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0
|
||||||
|
\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0
|
||||||
|
\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2
|
||||||
|
\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw
|
||||||
|
\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}
|
||||||
|
\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22
|
||||||
|
\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}\pard\plain\itap0\s0\ilvl0\fi0\sb0\sa0\aspalpha\aspnum\adjustright\brdrt\brdrl\brdrb
|
||||||
|
\brdrr\brdrbtw\brdrbar\widctlpar\ltrpar\li0\lin0\ri0\rin0\ql\faauto\sl276\slmult1\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1{\rtlch\ab0\ai0\af2\alang1025\afs22\ltrch\b0\i0\fs22\lang1033\langnp1033\langfe1033\langfenp1033
|
||||||
|
\loch\af2\dbch\af2\hich\f2\strike0\ulnone\cf1 C}{\rtlch\ab0\ai0\af2\afs22\ltrch\b0\i0\fs22\loch\af2\dbch\af2\hich\f2\insrsid10976062\strike0\ulnone\cf1\par}{
|
||||||
|
\*\latentstyles\lsdstimax267\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef0{\lsdlockedexcept\lsdqformat1 Normal;\lsdqformat1 heading 1;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 2;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 3;
|
||||||
|
\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 4;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 5;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 6;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 7;\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 8;
|
||||||
|
\lsdsemihidden1\lsdunhideused1\lsdqformat1 heading 9;\lsdsemihidden1\lsdunhideused1\lsdqformat1 caption;\lsdqformat1 Title;\lsdqformat1 Subtitle;\lsdqformat1 Strong;\lsdqformat1 Emphasis;\lsdsemihidden1\lsdpriority99 Placeholder Text;\lsdqformat1\lsdpriority1 No Spacing;
|
||||||
|
\lsdpriority60 Light Shading;\lsdpriority61 Light List;\lsdpriority62 Light Grid;\lsdpriority63 Medium Shading 1;\lsdpriority64 Medium Shading 2;\lsdpriority65 Medium List 1;\lsdpriority66 Medium List 2;\lsdpriority67 Medium Grid 1;\lsdpriority68 Medium Grid 2;
|
||||||
|
\lsdpriority69 Medium Grid 3;\lsdpriority70 Dark List;\lsdpriority71 Colorful Shading;\lsdpriority72 Colorful List;\lsdpriority73 Colorful Grid;\lsdpriority60 Light Shading Accent 1;\lsdpriority61 Light List Accent 1;\lsdpriority62 Light Grid Accent 1;\lsdpriority63 Medium Shading 1 Accent 1;
|
||||||
|
\lsdpriority64 Medium Shading 2 Accent 1;\lsdpriority65 Medium List 1 Accent 1;\lsdsemihidden1\lsdpriority99 Revision;\lsdqformat1\lsdpriority34 List Paragraph;\lsdqformat1\lsdpriority29 Quote;\lsdqformat1\lsdpriority30 Intense Quote;\lsdpriority66 Medium List 2 Accent 1;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 1;\lsdpriority68 Medium Grid 2 Accent 1;\lsdpriority69 Medium Grid 3 Accent 1;\lsdpriority70 Dark List Accent 1;\lsdpriority71 Colorful Shading Accent 1;\lsdpriority72 Colorful List Accent 1;\lsdpriority73 Colorful Grid Accent 1;
|
||||||
|
\lsdpriority60 Light Shading Accent 2;\lsdpriority61 Light List Accent 2;\lsdpriority62 Light Grid Accent 2;\lsdpriority63 Medium Shading 1 Accent 2;\lsdpriority64 Medium Shading 2 Accent 2;\lsdpriority65 Medium List 1 Accent 2;\lsdpriority66 Medium List 2 Accent 2;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 2;\lsdpriority68 Medium Grid 2 Accent 2;\lsdpriority69 Medium Grid 3 Accent 2;\lsdpriority70 Dark List Accent 2;\lsdpriority71 Colorful Shading Accent 2;\lsdpriority72 Colorful List Accent 2;\lsdpriority73 Colorful Grid Accent 2;
|
||||||
|
\lsdpriority60 Light Shading Accent 3;\lsdpriority61 Light List Accent 3;\lsdpriority62 Light Grid Accent 3;\lsdpriority63 Medium Shading 1 Accent 3;\lsdpriority64 Medium Shading 2 Accent 3;\lsdpriority65 Medium List 1 Accent 3;\lsdpriority66 Medium List 2 Accent 3;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 3;\lsdpriority68 Medium Grid 2 Accent 3;\lsdpriority69 Medium Grid 3 Accent 3;\lsdpriority70 Dark List Accent 3;\lsdpriority71 Colorful Shading Accent 3;\lsdpriority72 Colorful List Accent 3;\lsdpriority73 Colorful Grid Accent 3;
|
||||||
|
\lsdpriority60 Light Shading Accent 4;\lsdpriority61 Light List Accent 4;\lsdpriority62 Light Grid Accent 4;\lsdpriority63 Medium Shading 1 Accent 4;\lsdpriority64 Medium Shading 2 Accent 4;\lsdpriority65 Medium List 1 Accent 4;\lsdpriority66 Medium List 2 Accent 4;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 4;\lsdpriority68 Medium Grid 2 Accent 4;\lsdpriority69 Medium Grid 3 Accent 4;\lsdpriority70 Dark List Accent 4;\lsdpriority71 Colorful Shading Accent 4;\lsdpriority72 Colorful List Accent 4;\lsdpriority73 Colorful Grid Accent 4;
|
||||||
|
\lsdpriority60 Light Shading Accent 5;\lsdpriority61 Light List Accent 5;\lsdpriority62 Light Grid Accent 5;\lsdpriority63 Medium Shading 1 Accent 5;\lsdpriority64 Medium Shading 2 Accent 5;\lsdpriority65 Medium List 1 Accent 5;\lsdpriority66 Medium List 2 Accent 5;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 5;\lsdpriority68 Medium Grid 2 Accent 5;\lsdpriority69 Medium Grid 3 Accent 5;\lsdpriority70 Dark List Accent 5;\lsdpriority71 Colorful Shading Accent 5;\lsdpriority72 Colorful List Accent 5;\lsdpriority73 Colorful Grid Accent 5;
|
||||||
|
\lsdpriority60 Light Shading Accent 6;\lsdpriority61 Light List Accent 6;\lsdpriority62 Light Grid Accent 6;\lsdpriority63 Medium Shading 1 Accent 6;\lsdpriority64 Medium Shading 2 Accent 6;\lsdpriority65 Medium List 1 Accent 6;\lsdpriority66 Medium List 2 Accent 6;
|
||||||
|
\lsdpriority67 Medium Grid 1 Accent 6;\lsdpriority68 Medium Grid 2 Accent 6;\lsdpriority69 Medium Grid 3 Accent 6;\lsdpriority70 Dark List Accent 6;\lsdpriority71 Colorful Shading Accent 6;\lsdpriority72 Colorful List Accent 6;\lsdpriority73 Colorful Grid Accent 6;
|
||||||
|
\lsdqformat1\lsdpriority19 Subtle Emphasis;\lsdqformat1\lsdpriority21 Intense Emphasis;\lsdqformat1\lsdpriority31 Subtle Reference;\lsdqformat1\lsdpriority32 Intense Reference;\lsdqformat1\lsdpriority33 Book Title;\lsdsemihidden1\lsdunhideused1\lsdpriority37 Bibliography;
|
||||||
|
\lsdsemihidden1\lsdunhideused1\lsdqformat1\lsdpriority39 TOC Heading;}}}
|
16
cucumber/features/environment.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def before_all(context):
|
||||||
|
context.endpoint = None
|
||||||
|
context.request_data = None
|
||||||
|
context.files = {}
|
||||||
|
context.response = None
|
||||||
|
|
||||||
|
def after_scenario(context, scenario):
|
||||||
|
if hasattr(context, 'files'):
|
||||||
|
for file in context.files.values():
|
||||||
|
file.close()
|
||||||
|
if os.path.exists('response_file'):
|
||||||
|
os.remove('response_file')
|
||||||
|
if hasattr(context, 'file_name') and os.path.exists(context.file_name):
|
||||||
|
os.remove(context.file_name)
|
130
cucumber/features/examples.feature
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
@example
|
||||||
|
Feature: API Validation
|
||||||
|
|
||||||
|
@positive @password
|
||||||
|
Scenario: Remove password
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages
|
||||||
|
And the pdf is encrypted with password "password123"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| password | password123 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/remove-password"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response PDF is not passworded
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@negative @password
|
||||||
|
Scenario: Remove password wrong password
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages
|
||||||
|
And the pdf is encrypted with password "password123"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| password | wrongPassword |
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/remove-password"
|
||||||
|
Then the response status code should be 500
|
||||||
|
And the response should contain error message "Internal Server Error"
|
||||||
|
|
||||||
|
@positive @info
|
||||||
|
Scenario: Get info
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/get-info-on-pdf"
|
||||||
|
Then the response content type should be "application/json"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@positive @password
|
||||||
|
Scenario: Add password
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| password | password123 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/add-password"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response PDF is passworded
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@positive @password
|
||||||
|
Scenario: Add password with other params
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| ownerPassword | ownerPass |
|
||||||
|
| password | password123 |
|
||||||
|
| keyLength | 256 |
|
||||||
|
| canPrint | true |
|
||||||
|
| canModify | false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/add-password"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response PDF is passworded
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@positive @watermark
|
||||||
|
Scenario: Add watermark
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| watermarkType | text |
|
||||||
|
| watermarkText | Sample Watermark |
|
||||||
|
| fontSize | 30 |
|
||||||
|
| rotation | 45 |
|
||||||
|
| opacity | 0.5 |
|
||||||
|
| widthSpacer | 50 |
|
||||||
|
| heightSpacer | 50 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/security/add-watermark"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@positive
|
||||||
|
Scenario: Remove blank pages
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 blank pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| threshold | 90 |
|
||||||
|
| whitePercent | 99.9 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
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
|
||||||
|
Scenario: Flatten PDF
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| flattenOnlyForms | false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/flatten"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@positive @metadata
|
||||||
|
Scenario: Update metadata
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| author | John Doe |
|
||||||
|
| title | Sample Title |
|
||||||
|
| subject | Sample Subject |
|
||||||
|
| keywords | sample, test |
|
||||||
|
| producer | Test Producer |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/update-metadata"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response PDF metadata should include "Author" as "John Doe"
|
||||||
|
And the response PDF metadata should include "Keywords" as "sample, test"
|
||||||
|
And the response PDF metadata should include "Subject" as "Sample Subject"
|
||||||
|
And the response PDF metadata should include "Title" as "Sample Title"
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
228
cucumber/features/external.feature
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
Feature: API Validation
|
||||||
|
|
||||||
|
|
||||||
|
@libre @positive
|
||||||
|
Scenario: Repair PDF
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/repair"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
||||||
|
@ocr @positive
|
||||||
|
Scenario: Process PDF with OCR
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| languages | eng |
|
||||||
|
| sidecar | false |
|
||||||
|
| deskew | true |
|
||||||
|
| clean | true |
|
||||||
|
| cleanFinal | true |
|
||||||
|
| ocrType | Normal |
|
||||||
|
| ocrRenderType | hocr |
|
||||||
|
| removeImagesAfter| false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
||||||
|
@ocr @positive
|
||||||
|
Scenario: Extract Image Scans
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 images on 2 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| angleThreshold | 5 |
|
||||||
|
| tolerance | 20 |
|
||||||
|
| minArea | 8000 |
|
||||||
|
| minContourArea | 500 |
|
||||||
|
| borderSize | 1 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/extract-image-scans"
|
||||||
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response file should have extension ".zip"
|
||||||
|
And the response ZIP should contain 2 files
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ocr @negative
|
||||||
|
Scenario: Process PDF with text and OCR with type normal
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| languages | eng |
|
||||||
|
| sidecar | false |
|
||||||
|
| deskew | true |
|
||||||
|
| clean | true |
|
||||||
|
| cleanFinal | true |
|
||||||
|
| ocrType | Normal |
|
||||||
|
| ocrRenderType | hocr |
|
||||||
|
| removeImagesAfter| false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||||
|
Then the response status code should be 500
|
||||||
|
|
||||||
|
@ocr @positive
|
||||||
|
Scenario: Process PDF with OCR
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| languages | eng |
|
||||||
|
| sidecar | false |
|
||||||
|
| deskew | true |
|
||||||
|
| clean | true |
|
||||||
|
| cleanFinal | true |
|
||||||
|
| ocrType | Force |
|
||||||
|
| ocrRenderType | hocr |
|
||||||
|
| removeImagesAfter| false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
@ocr @positive
|
||||||
|
Scenario: Process PDF with OCR with sidecar
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| languages | eng |
|
||||||
|
| sidecar | true |
|
||||||
|
| deskew | true |
|
||||||
|
| clean | true |
|
||||||
|
| cleanFinal | true |
|
||||||
|
| ocrType | Force |
|
||||||
|
| ocrRenderType | hocr |
|
||||||
|
| removeImagesAfter| false |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/ocr-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 2 files
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
|
||||||
|
@libre @positive
|
||||||
|
Scenario Outline: Convert PDF to various word formats
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| outputFormat | <format> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/convert/pdf/word"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response file should have extension "<extension>"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| format | extension |
|
||||||
|
| docx | .docx |
|
||||||
|
| odt | .odt |
|
||||||
|
| doc | .doc |
|
||||||
|
|
||||||
|
@ocr
|
||||||
|
Scenario: PDFA
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| outputFormat | pdfa |
|
||||||
|
When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
@ocr
|
||||||
|
Scenario: PDFA1
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| outputFormat | pdfa-1 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/convert/pdf/pdfa"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
@compress @ghostscript @positive
|
||||||
|
Scenario: Compress
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| optimizeLevel | 4 |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/compress-pdf"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
@compress @ghostscript @positive
|
||||||
|
Scenario: Compress
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| optimizeLevel | 1 |
|
||||||
|
| expectedOutputSize | 5KB |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/compress-pdf"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
|
||||||
|
@compress @ghostscript @positive
|
||||||
|
Scenario: Compress
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| optimizeLevel | 1 |
|
||||||
|
| expectedOutputSize | 5KB |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/compress-pdf"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
@libre @positive
|
||||||
|
Scenario Outline: Convert PDF to various types
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 3 pages with random text
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| outputFormat | <format> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/convert/pdf/<type>"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response file should have extension "<extension>"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| type | format | extension |
|
||||||
|
| text | rtf | .rtf |
|
||||||
|
| text | txt | .txt |
|
||||||
|
| presentation | ppt | .ppt |
|
||||||
|
| presentation | pptx | .pptx |
|
||||||
|
| presentation | odp | .odp |
|
||||||
|
| html | html | .zip |
|
||||||
|
|
||||||
|
|
||||||
|
@libre @positive @topdf
|
||||||
|
Scenario Outline: Convert PDF to various types
|
||||||
|
Given I use an example file at "exampleFiles/example<extension>" as parameter "fileInput"
|
||||||
|
When I send the API request to the endpoint "/api/v1/convert/file/pdf"
|
||||||
|
Then the response status code should be 200
|
||||||
|
And the response file should have size greater than 100
|
||||||
|
And the response file should have extension ".pdf"
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| extension |
|
||||||
|
| .docx |
|
||||||
|
| .odp |
|
||||||
|
| .odt |
|
||||||
|
| .pptx |
|
||||||
|
| .rtf |
|
||||||
|
|
||||||
|
|
||||||
|
|
96
cucumber/features/general.feature
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
@general
|
||||||
|
Feature: API Validation
|
||||||
|
|
||||||
|
|
||||||
|
@split-pdf-by-sections @positive
|
||||||
|
Scenario Outline: split-pdf-by-sections with different parameters
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 2 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| horizontalDivisions | <horizontalDivisions> |
|
||||||
|
| verticalDivisions | <verticalDivisions> |
|
||||||
|
| merge | true |
|
||||||
|
When I send the API request to the endpoint "/api/v1/general/split-pdf-by-sections"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 200
|
||||||
|
And the response status code should be 200
|
||||||
|
And the response PDF should contain <page_count> pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| horizontalDivisions | verticalDivisions | page_count |
|
||||||
|
| 0 | 1 | 4 |
|
||||||
|
| 1 | 1 | 8 |
|
||||||
|
| 1 | 2 | 12 |
|
||||||
|
| 2 | 2 | 18 |
|
||||||
|
|
||||||
|
@split-pdf-by-sections @positive
|
||||||
|
Scenario Outline: split-pdf-by-sections with different parameters
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 2 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| horizontalDivisions | <horizontalDivisions> |
|
||||||
|
| verticalDivisions | <verticalDivisions> |
|
||||||
|
| merge | true |
|
||||||
|
When I send the API request to the endpoint "/api/v1/general/split-pdf-by-sections"
|
||||||
|
Then the response content type should be "application/pdf"
|
||||||
|
And the response file should have size greater than 200
|
||||||
|
And the response status code should be 200
|
||||||
|
And the response PDF should contain <page_count> pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| horizontalDivisions | verticalDivisions | page_count |
|
||||||
|
| 0 | 1 | 4 |
|
||||||
|
| 1 | 1 | 8 |
|
||||||
|
| 1 | 2 | 12 |
|
||||||
|
| 2 | 2 | 18 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@split-pdf-by-pages @positive
|
||||||
|
Scenario Outline: split-pdf-by-pages with different parameters
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 20 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| fileInput | fileInput |
|
||||||
|
| pageNumbers | <pageNumbers> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/general/split-pages"
|
||||||
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response status code should be 200
|
||||||
|
And the response file should have size greater than 200
|
||||||
|
And the response ZIP should contain <file_count> files
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| pageNumbers | file_count |
|
||||||
|
| 1,3,5-9 | 8 |
|
||||||
|
| all | 20 |
|
||||||
|
| 2n+1 | 11 |
|
||||||
|
| 3n | 7 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@split-pdf-by-size-or-count @positive
|
||||||
|
Scenario Outline: split-pdf-by-size-or-count with different parameters
|
||||||
|
Given I generate a PDF file as "fileInput"
|
||||||
|
And the pdf contains 20 pages
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| fileInput | fileInput |
|
||||||
|
| splitType | <splitType> |
|
||||||
|
| splitValue | <splitValue> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/general/split-by-size-or-count"
|
||||||
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response status code should be 200
|
||||||
|
And the response file should have size greater than 200
|
||||||
|
And the response ZIP file should contain <doc_count> documents each having <pages_per_doc> pages
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| splitType | splitValue | doc_count | pages_per_doc |
|
||||||
|
| 1 | 5 | 4 | 5 |
|
||||||
|
| 2 | 2 | 2 | 10 |
|
||||||
|
| 2 | 4 | 4 | 5 |
|
||||||
|
| 1 | 10 | 2 | 10 |
|
||||||
|
|
||||||
|
|
307
cucumber/features/steps/step_definitions.py
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from behave import given, when, then
|
||||||
|
from PyPDF2 import PdfWriter, PdfReader
|
||||||
|
import io
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.pdfgen import canvas
|
||||||
|
import mimetypes
|
||||||
|
import requests
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
#########
|
||||||
|
# GIVEN #
|
||||||
|
#########
|
||||||
|
|
||||||
|
@given('I generate a PDF file as "{fileInput}"')
|
||||||
|
def step_generate_pdf(context, fileInput):
|
||||||
|
context.param_name = fileInput
|
||||||
|
context.file_name = "genericNonCustomisableName.pdf"
|
||||||
|
writer = PdfWriter()
|
||||||
|
writer.add_blank_page(width=72, height=72) # Single blank page
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
if not hasattr(context, 'files'):
|
||||||
|
context.files = {}
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
@given('I use an example file at "{filePath}" as parameter "{fileInput}"')
|
||||||
|
def step_use_example_file(context, filePath, fileInput):
|
||||||
|
context.param_name = fileInput
|
||||||
|
context.file_name = filePath.split('/')[-1]
|
||||||
|
if not hasattr(context, 'files'):
|
||||||
|
context.files = {}
|
||||||
|
|
||||||
|
# Ensure the file exists before opening
|
||||||
|
try:
|
||||||
|
example_file = open(filePath, 'rb')
|
||||||
|
context.files[context.param_name] = example_file
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@given('the pdf contains {page_count:d} pages')
|
||||||
|
def step_pdf_contains_pages(context, page_count):
|
||||||
|
writer = PdfWriter()
|
||||||
|
for i in range(page_count):
|
||||||
|
writer.add_blank_page(width=72, height=72)
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
# Duplicate for now...
|
||||||
|
@given('the pdf contains {page_count:d} blank pages')
|
||||||
|
def step_pdf_contains_blank_pages(context, page_count):
|
||||||
|
writer = PdfWriter()
|
||||||
|
for i in range(page_count):
|
||||||
|
writer.add_blank_page(width=72, height=72)
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def create_black_box_image(file_name, size):
|
||||||
|
can = canvas.Canvas(file_name, pagesize=size)
|
||||||
|
width, height = size
|
||||||
|
can.setFillColorRGB(0, 0, 0)
|
||||||
|
can.rect(0, 0, width, height, fill=1)
|
||||||
|
can.showPage()
|
||||||
|
can.save()
|
||||||
|
|
||||||
|
def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
||||||
|
page_width, page_height = letter
|
||||||
|
box_size = 72 # 1 inch by 1 inch black box
|
||||||
|
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
|
||||||
|
|
||||||
|
writer = PdfWriter()
|
||||||
|
box_counter = 0
|
||||||
|
|
||||||
|
for page in range(page_count):
|
||||||
|
packet = io.BytesIO()
|
||||||
|
can = canvas.Canvas(packet, pagesize=letter)
|
||||||
|
|
||||||
|
for i in range(boxes_per_page):
|
||||||
|
if box_counter >= image_count:
|
||||||
|
break
|
||||||
|
x = (i % (page_width // box_size)) * box_size
|
||||||
|
y = page_height - ((i // (page_width // box_size) + 1) * box_size)
|
||||||
|
can.setFillColorRGB(0, 0, 0)
|
||||||
|
can.rect(x, y, box_size, box_size, fill=1)
|
||||||
|
box_counter += 1
|
||||||
|
|
||||||
|
can.showPage()
|
||||||
|
can.save()
|
||||||
|
packet.seek(0)
|
||||||
|
new_pdf = PdfReader(packet)
|
||||||
|
writer.add_page(new_pdf.pages[0])
|
||||||
|
|
||||||
|
with open(file_name, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
|
||||||
|
@given('the pdf contains {image_count:d} images on {page_count:d} pages')
|
||||||
|
def step_pdf_contains_images(context, image_count, page_count):
|
||||||
|
if not hasattr(context, 'param_name'):
|
||||||
|
context.param_name = "default"
|
||||||
|
context.file_name = "genericNonCustomisableName.pdf"
|
||||||
|
create_pdf_with_black_boxes(context.file_name, image_count, page_count)
|
||||||
|
if not hasattr(context, 'files'):
|
||||||
|
context.files = {}
|
||||||
|
if context.param_name in context.files:
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
@given('the pdf contains {page_count:d} pages with random text')
|
||||||
|
def step_pdf_contains_pages_with_random_text(context, page_count):
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
c = canvas.Canvas(buffer, pagesize=letter)
|
||||||
|
width, height = letter
|
||||||
|
|
||||||
|
for _ in range(page_count):
|
||||||
|
text = ''.join(random.choices(string.ascii_letters + string.digits, k=100))
|
||||||
|
c.drawString(100, height - 100, text)
|
||||||
|
c.showPage()
|
||||||
|
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
f.write(buffer.getvalue())
|
||||||
|
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
@given('the pdf pages all contain the text "{text}"')
|
||||||
|
def step_pdf_pages_contain_text(context, text):
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
c = canvas.Canvas(buffer, pagesize=letter)
|
||||||
|
width, height = letter
|
||||||
|
|
||||||
|
for _ in range(len(PdfReader(context.file_name).pages)):
|
||||||
|
c.drawString(100, height - 100, text)
|
||||||
|
c.showPage()
|
||||||
|
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
f.write(buffer.getvalue())
|
||||||
|
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
@given('the pdf is encrypted with password "{password}"')
|
||||||
|
def step_encrypt_pdf(context, password):
|
||||||
|
writer = PdfWriter()
|
||||||
|
reader = PdfReader(context.file_name)
|
||||||
|
for i in range(len(reader.pages)):
|
||||||
|
writer.add_page(reader.pages[i])
|
||||||
|
writer.encrypt(password)
|
||||||
|
with open(context.file_name, 'wb') as f:
|
||||||
|
writer.write(f)
|
||||||
|
context.files[context.param_name].close()
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
@given('the request data is')
|
||||||
|
def step_request_data(context):
|
||||||
|
context.request_data = eval(context.text)
|
||||||
|
|
||||||
|
@given('the request data includes')
|
||||||
|
def step_request_data_table(context):
|
||||||
|
context.request_data = {row['parameter']: row['value'] for row in context.table}
|
||||||
|
|
||||||
|
@given('save the generated PDF file as "{filename}" for debugging')
|
||||||
|
def save_generated_pdf(context, filename):
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(context.files[context.param_name].read())
|
||||||
|
print(f"Saved generated PDF content to {filename}")
|
||||||
|
|
||||||
|
########
|
||||||
|
# WHEN #
|
||||||
|
########
|
||||||
|
|
||||||
|
@when('I send the API request to the endpoint "{endpoint}"')
|
||||||
|
def step_send_api_request(context, endpoint):
|
||||||
|
url = f"http://localhost:8080{endpoint}"
|
||||||
|
files = context.files if hasattr(context, 'files') else {}
|
||||||
|
|
||||||
|
if not hasattr(context, 'request_data') or context.request_data is None:
|
||||||
|
context.request_data = {}
|
||||||
|
|
||||||
|
form_data = []
|
||||||
|
for key, value in context.request_data.items():
|
||||||
|
form_data.append((key, (None, value)))
|
||||||
|
|
||||||
|
for key, file in files.items():
|
||||||
|
mime_type, _ = mimetypes.guess_type(file.name)
|
||||||
|
mime_type = mime_type or 'application/octet-stream'
|
||||||
|
print(f"form_data {file.name} with {mime_type}")
|
||||||
|
form_data.append((key, (file.name, file, mime_type)))
|
||||||
|
|
||||||
|
response = requests.post(url, files=form_data)
|
||||||
|
context.response = response
|
||||||
|
|
||||||
|
########
|
||||||
|
# THEN #
|
||||||
|
########
|
||||||
|
|
||||||
|
@then('the response content type should be "{content_type}"')
|
||||||
|
def step_check_response_content_type(context, content_type):
|
||||||
|
actual_content_type = context.response.headers.get('Content-Type', '')
|
||||||
|
assert actual_content_type.startswith(content_type), f"Expected {content_type} but got {actual_content_type}. Response content: {context.response.content}"
|
||||||
|
|
||||||
|
@then('the response file should have size greater than {size:d}')
|
||||||
|
def step_check_response_file_size(context, size):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
assert len(response_file.getvalue()) > size
|
||||||
|
|
||||||
|
@then('the response PDF is not passworded')
|
||||||
|
def step_check_response_pdf_not_passworded(context):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
reader = PdfReader(response_file)
|
||||||
|
assert not reader.is_encrypted
|
||||||
|
|
||||||
|
@then('the response PDF is passworded')
|
||||||
|
def step_check_response_pdf_passworded(context):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
try:
|
||||||
|
reader = PdfReader(response_file)
|
||||||
|
assert reader.is_encrypted
|
||||||
|
except PdfReadError as e:
|
||||||
|
raise AssertionError(f"Failed to read PDF: {str(e)}. Response content: {context.response.content}")
|
||||||
|
except Exception as e:
|
||||||
|
raise AssertionError(f"An error occurred: {str(e)}. Response content: {context.response.content}")
|
||||||
|
|
||||||
|
@then('the response status code should be {status_code:d}')
|
||||||
|
def step_check_response_status_code(context, status_code):
|
||||||
|
assert context.response.status_code == status_code, f"Expected status code {status_code} but got {context.response.status_code}"
|
||||||
|
|
||||||
|
@then('the response should contain error message "{message}"')
|
||||||
|
def step_check_response_error_message(context, message):
|
||||||
|
response_json = context.response.json()
|
||||||
|
assert response_json.get('error') == message, f"Expected error message '{message}' but got '{response_json.get('error')}'"
|
||||||
|
|
||||||
|
@then('the response PDF should contain {page_count:d} pages')
|
||||||
|
def step_check_response_pdf_page_count(context, page_count):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
reader = PdfReader(response_file)
|
||||||
|
assert len(reader.pages) == page_count, f"Expected {page_count} pages but got {len(reader.pages)} pages"
|
||||||
|
|
||||||
|
@then('the response PDF metadata should include "{metadata_key}" as "{metadata_value}"')
|
||||||
|
def step_check_response_pdf_metadata(context, metadata_key, metadata_value):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
reader = PdfReader(response_file)
|
||||||
|
metadata = reader.metadata
|
||||||
|
assert metadata.get("/" + metadata_key) == metadata_value, f"Expected {metadata_key} to be '{metadata_value}' but got '{metadata.get(metadata_key)}'"
|
||||||
|
|
||||||
|
@then('the response file should have extension "{extension}"')
|
||||||
|
def step_check_response_file_extension(context, extension):
|
||||||
|
content_disposition = context.response.headers.get('Content-Disposition', '')
|
||||||
|
filename = ""
|
||||||
|
if content_disposition:
|
||||||
|
parts = content_disposition.split(';')
|
||||||
|
for part in parts:
|
||||||
|
if part.strip().startswith('filename'):
|
||||||
|
filename = part.split('=')[1].strip().strip('"')
|
||||||
|
break
|
||||||
|
assert filename.endswith(extension), f"Expected file extension {extension} but got {filename}. Response content: {context.response.content}"
|
||||||
|
|
||||||
|
@then('save the response file as "{filename}" for debugging')
|
||||||
|
def step_save_response_file(context, filename):
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
f.write(context.response.content)
|
||||||
|
print(f"Saved response content to {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
@then('the response PDF should contain {page_count:d} pages')
|
||||||
|
def step_check_response_pdf_page_count(context, page_count):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
reader = PdfReader(io.BytesIO(response_file.getvalue()))
|
||||||
|
actual_page_count = len(reader.pages)
|
||||||
|
assert actual_page_count == page_count, f"Expected {page_count} pages but got {actual_page_count} pages"
|
||||||
|
|
||||||
|
@then('the response ZIP should contain {file_count:d} files')
|
||||||
|
def step_check_response_zip_file_count(context, file_count):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file:
|
||||||
|
actual_file_count = len(zip_file.namelist())
|
||||||
|
assert actual_file_count == file_count, f"Expected {file_count} files but got {actual_file_count} files"
|
||||||
|
|
||||||
|
@then('the response ZIP file should contain {doc_count:d} documents each having {pages_per_doc:d} pages')
|
||||||
|
def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc):
|
||||||
|
response_file = io.BytesIO(context.response.content)
|
||||||
|
with zipfile.ZipFile(io.BytesIO(response_file.getvalue())) as zip_file:
|
||||||
|
actual_doc_count = len(zip_file.namelist())
|
||||||
|
assert actual_doc_count == doc_count, f"Expected {doc_count} documents but got {actual_doc_count} documents"
|
||||||
|
|
||||||
|
for file_name in zip_file.namelist():
|
||||||
|
with zip_file.open(file_name) as pdf_file:
|
||||||
|
reader = PdfReader(pdf_file)
|
||||||
|
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}"
|
5
cucumber/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
behave
|
||||||
|
requests
|
||||||
|
PyPDF2
|
||||||
|
reportlab
|
||||||
|
PyCryptodome
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 50 KiB |
34
exampleYmlFiles/docker-compose-latest-fat-security.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
version: '3.3'
|
||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security-Fat
|
||||||
|
image: frooodle/s-pdf:latest-fat
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
|
- /stirling/latest/config:/configs:rw
|
||||||
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
restart: on-failure:5
|
@ -13,7 +13,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
@ -22,10 +22,13 @@ services:
|
|||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "true"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
SECURITY_OAUTH2_ENABLED: "true"
|
SECURITY_OAUTH2_ENABLED: "true"
|
||||||
SECURITY_OAUTH2_AUTOCREATEUSER: "true" # This is set to true to allow auto-creation of non-existing users in Striling-PDF
|
SECURITY_OAUTH2_AUTOCREATEUSER: "true" # This is set to true to allow auto-creation of non-existing users in Stirling-PDF
|
||||||
SECURITY_OAUTH2_ISSUER: "https://accounts.google.com" # Change with any other provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
SECURITY_OAUTH2_ISSUER: "https://accounts.google.com" # Change with any other provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
SECURITY_OAUTH2_CLIENTID: "<YOUR CLIENT ID>.apps.googleusercontent.com" # Client ID from your provider
|
SECURITY_OAUTH2_CLIENTID: "<YOUR CLIENT ID>.apps.googleusercontent.com" # Client ID from your provider
|
||||||
SECURITY_OAUTH2_CLIENTSECRET: "<YOUR CLIENT SECRET>" # Client Secret from your provider
|
SECURITY_OAUTH2_CLIENTSECRET: "<YOUR CLIENT SECRET>" # Client Secret from your provider
|
||||||
|
SECURITY_OAUTH2_SCOPES: "openid,profile,email" # Expected OAuth2 Scope
|
||||||
|
SECURITY_OAUTH2_USEASUSERNAME: "email" # Default is 'email'; custom fields can be used as the username
|
||||||
|
SECURITY_OAUTH2_PROVIDER: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
@ -13,7 +13,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
|
@ -13,7 +13,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
|
@ -13,7 +13,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- /stirling/latest/logs:/logs:rw
|
||||||
|
@ -13,7 +13,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- /stirling/latest/config:/configs:rw
|
||||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 44 KiB |
BIN
images/settings-light.png
Normal file
After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 12 KiB |
BIN
images/stirling-home-dark.png
Normal file
After Width: | Height: | Size: 366 KiB |
Before Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 203 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}"
|
||||||
|
}
|
@ -11,11 +11,13 @@ if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -
|
|||||||
fi
|
fi
|
||||||
umask "$UMASK" || true
|
umask "$UMASK" || true
|
||||||
|
|
||||||
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" ]]; then
|
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
|
||||||
apk add --no-cache calibre@testing
|
apk add --no-cache calibre@testing
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/scripts/download-security-jar.sh
|
if [[ "$FAT_DOCKER" != "true" ]]; then
|
||||||
|
/scripts/download-security-jar.sh
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "$LANGS" ]]; then
|
if [[ -n "$LANGS" ]]; then
|
||||||
/scripts/installFonts.sh $LANGS
|
/scripts/installFonts.sh $LANGS
|
||||||
|
@ -13,6 +13,14 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[cs_CZ]
|
||||||
|
ignore = [
|
||||||
|
'info',
|
||||||
|
'language.direction',
|
||||||
|
'pipeline.header',
|
||||||
|
'text',
|
||||||
|
]
|
||||||
|
|
||||||
[de_DE]
|
[de_DE]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
@ -53,6 +61,7 @@ ignore = [
|
|||||||
[fr_FR]
|
[fr_FR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'sponsor',
|
||||||
]
|
]
|
||||||
|
|
||||||
[hi_IN]
|
[hi_IN]
|
||||||
@ -60,6 +69,16 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[hr_HR]
|
||||||
|
ignore = [
|
||||||
|
'font',
|
||||||
|
'home.pipeline.title',
|
||||||
|
'info',
|
||||||
|
'language.direction',
|
||||||
|
'pdfOrganiser.tags',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
[hu_HU]
|
[hu_HU]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@ -98,6 +117,11 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[no_NB]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[pl_PL]
|
[pl_PL]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@ -123,6 +147,11 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[sk_SK]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[sr_LATN_RS]
|
[sr_LATN_RS]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
@ -6,11 +6,15 @@ import java.net.Socket;
|
|||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
public class LibreOfficeListener {
|
public class LibreOfficeListener {
|
||||||
|
|
||||||
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
|
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
||||||
|
private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes
|
||||||
|
|
||||||
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
|
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
|
||||||
private static final int LISTENER_PORT = 2002;
|
private static final int LISTENER_PORT = 2002;
|
||||||
@ -27,14 +31,12 @@ public class LibreOfficeListener {
|
|||||||
private LibreOfficeListener() {}
|
private LibreOfficeListener() {}
|
||||||
|
|
||||||
private boolean isListenerRunning() {
|
private boolean isListenerRunning() {
|
||||||
try {
|
|
||||||
System.out.println("waiting for listener to start");
|
System.out.println("waiting for listener to start");
|
||||||
Socket socket = new Socket();
|
try (Socket socket = new Socket()) {
|
||||||
socket.connect(
|
socket.connect(
|
||||||
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||||
socket.close();
|
|
||||||
return true;
|
return true;
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,6 +65,7 @@ public class LibreOfficeListener {
|
|||||||
try {
|
try {
|
||||||
Thread.sleep(5000); // Check for inactivity every 5 seconds
|
Thread.sleep(5000); // Check for inactivity every 5 seconds
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,8 +83,8 @@ public class LibreOfficeListener {
|
|||||||
try {
|
try {
|
||||||
Thread.sleep(1000);
|
Thread.sleep(1000);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// TODO Auto-generated catch block
|
Thread.currentThread().interrupt();
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
} // Check every 1 second
|
} // Check every 1 second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ 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.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@ -65,14 +67,36 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
|
// stirling pdf settings file
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
app.setDefaultProperties(
|
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||||
Collections.singletonMap(
|
|
||||||
"spring.config.additional-location", "file:configs/settings.yml"));
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom javs settings file
|
||||||
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
|
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
|
if (!existing.isEmpty()) {
|
||||||
|
existing += ",";
|
||||||
|
}
|
||||||
|
propertyFiles.put(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
existing + "file:configs/custom_settings.yml");
|
||||||
|
} else {
|
||||||
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!propertyFiles.isEmpty()) {
|
||||||
|
app.setDefaultProperties(
|
||||||
|
Collections.singletonMap(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
|
}
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -2,9 +2,13 @@ package stirling.software.SPDF.config;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
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.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -22,6 +26,8 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
@Lazy
|
@Lazy
|
||||||
public class AppConfig {
|
public class AppConfig {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ -54,7 +60,7 @@ public class AppConfig {
|
|||||||
props.load(resource.getInputStream());
|
props.load(resource.getInputStream());
|
||||||
return props.getProperty("version");
|
return props.getProperty("version");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
return "0.0.0";
|
return "0.0.0";
|
||||||
}
|
}
|
||||||
@ -108,4 +114,26 @@ public class AppConfig {
|
|||||||
public boolean missingActivSecurity() {
|
public boolean missingActivSecurity() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "watchedFoldersDir")
|
||||||
|
public String watchedFoldersDir() {
|
||||||
|
return "./pipeline/watchedFolders/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "finishedFoldersDir")
|
||||||
|
public String finishedFoldersDir() {
|
||||||
|
return "./pipeline/finishedFolders/";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "directoryFilter")
|
||||||
|
public Predicate<Path> processPDFOnlyFilter() {
|
||||||
|
return path -> {
|
||||||
|
if (Files.isDirectory(path)) {
|
||||||
|
return !path.toString().contains("processing");
|
||||||
|
} else {
|
||||||
|
String fileName = path.getFileName().toString();
|
||||||
|
return fileName.endsWith(".pdf");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,14 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
|
|
||||||
private static final List<String> ALLOWED_PARAMS =
|
private static final List<String> ALLOWED_PARAMS =
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
|
"lang",
|
||||||
|
"endpoint",
|
||||||
|
"endpoints",
|
||||||
|
"logout",
|
||||||
|
"error",
|
||||||
|
"erroroauth",
|
||||||
|
"file",
|
||||||
|
"messageType");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
@ -51,7 +58,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
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.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
@ -26,12 +19,12 @@ public class ConfigInitializer
|
|||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
try {
|
try {
|
||||||
ensureConfigExists();
|
ensureConfigExists();
|
||||||
} catch (IOException e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to initialize application configuration", e);
|
throw new RuntimeException("Failed to initialize application configuration", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ensureConfigExists() throws IOException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// Define the path to the external config directory
|
// Define the path to the external config directory
|
||||||
Path destPath = Paths.get("configs", "settings.yml");
|
Path destPath = Paths.get("configs", "settings.yml");
|
||||||
|
|
||||||
@ -51,170 +44,95 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If user file exists, we need to merge it with the template from the classpath
|
// Path templatePath =
|
||||||
List<String> templateLines;
|
// Paths.get(
|
||||||
try (InputStream in =
|
// getClass()
|
||||||
getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
|
// .getClassLoader()
|
||||||
templateLines =
|
// .getResource("settings.yml.template")
|
||||||
new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
|
// .toURI());
|
||||||
.lines()
|
// Path userPath = Paths.get("configs", "settings.yml");
|
||||||
.collect(Collectors.toList());
|
//
|
||||||
|
// List<String> templateLines = Files.readAllLines(templatePath);
|
||||||
|
// List<String> userLines =
|
||||||
|
// Files.exists(userPath) ? Files.readAllLines(userPath) : new
|
||||||
|
// ArrayList<>();
|
||||||
|
//
|
||||||
|
// List<String> resultLines = new ArrayList<>();
|
||||||
|
// int position = 0;
|
||||||
|
// for (String templateLine : templateLines) {
|
||||||
|
// // Check if the line is a comment
|
||||||
|
// if (templateLine.trim().startsWith("#")) {
|
||||||
|
// String entry = templateLine.trim().substring(1).trim();
|
||||||
|
// if (!entry.isEmpty()) {
|
||||||
|
// // Check if this comment has been uncommented in userLines
|
||||||
|
// String key = entry.split(":")[0].trim();
|
||||||
|
// addLine(resultLines, userLines, templateLine, key, position);
|
||||||
|
// } else {
|
||||||
|
// resultLines.add(templateLine);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// // Check if the line is a key-value pair
|
||||||
|
// else if (templateLine.contains(":")) {
|
||||||
|
// String key = templateLine.split(":")[0].trim();
|
||||||
|
// addLine(resultLines, userLines, templateLine, key, position);
|
||||||
|
// }
|
||||||
|
// // Handle empty lines
|
||||||
|
// else if (templateLine.trim().length() == 0) {
|
||||||
|
// resultLines.add("");
|
||||||
|
// }
|
||||||
|
// position++;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Write the result to the user settings file
|
||||||
|
// Files.write(userPath, resultLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeYamlFiles(templateLines, destPath, destPath);
|
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||||
|
if (!Files.exists(customSettingsPath)) {
|
||||||
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
|
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
||||||
throws IOException {
|
private static void addLine(
|
||||||
List<String> userLines = Files.readAllLines(userFilePath);
|
List<String> resultLines,
|
||||||
List<String> mergedLines = new ArrayList<>();
|
|
||||||
boolean insideAutoGenerated = false;
|
|
||||||
boolean beforeFirstKey = true;
|
|
||||||
|
|
||||||
Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
|
|
||||||
Function<String, String> extractKey =
|
|
||||||
line -> {
|
|
||||||
String[] parts = line.split(":");
|
|
||||||
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
|
|
||||||
};
|
|
||||||
|
|
||||||
Function<String, Integer> getIndentationLevel =
|
|
||||||
line -> {
|
|
||||||
int count = 0;
|
|
||||||
for (char ch : line.toCharArray()) {
|
|
||||||
if (ch == ' ') count++;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
for (String line : templateLines) {
|
|
||||||
String key = extractKey.apply(line);
|
|
||||||
|
|
||||||
if ("AutomaticallyGenerated:".equalsIgnoreCase(line.trim())) {
|
|
||||||
insideAutoGenerated = true;
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
} else if (insideAutoGenerated && line.trim().isEmpty()) {
|
|
||||||
insideAutoGenerated = false;
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
|
|
||||||
// Handle top comments and empty lines before the first key.
|
|
||||||
mergedLines.add(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!key.isEmpty()) beforeFirstKey = false;
|
|
||||||
|
|
||||||
if (userKeys.contains(key)) {
|
|
||||||
// If user has any version (commented or uncommented) of this key, skip the
|
|
||||||
// template line
|
|
||||||
Optional<String> userValue =
|
|
||||||
userLines.stream()
|
|
||||||
.filter(
|
|
||||||
l ->
|
|
||||||
extractKey.apply(l).equalsIgnoreCase(key)
|
|
||||||
&& !isCommented.apply(l))
|
|
||||||
.findFirst();
|
|
||||||
if (userValue.isPresent()) mergedLines.add(userValue.get());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
|
|
||||||
mergedLines.add(
|
|
||||||
line); // If line is commented, empty or key not present in user's file,
|
|
||||||
// retain the
|
|
||||||
// template line
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any additional uncommented user lines that are not present in the
|
|
||||||
// template
|
|
||||||
for (String userLine : userLines) {
|
|
||||||
String userKey = extractKey.apply(userLine);
|
|
||||||
boolean isPresentInTemplate =
|
|
||||||
templateLines.stream()
|
|
||||||
.map(extractKey)
|
|
||||||
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
|
|
||||||
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
|
|
||||||
if (!childOfTemplateEntry(
|
|
||||||
isCommented,
|
|
||||||
extractKey,
|
|
||||||
getIndentationLevel,
|
|
||||||
userLines,
|
|
||||||
userLine,
|
|
||||||
templateLines)) {
|
|
||||||
// check if userLine is a child of a entry within templateLines or not, if child
|
|
||||||
// of parent in templateLines then dont add to mergedLines, if anything else
|
|
||||||
// then add
|
|
||||||
mergedLines.add(userLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
|
|
||||||
}
|
|
||||||
|
|
||||||
// New method to check if a userLine is a child of an entry in templateLines
|
|
||||||
boolean childOfTemplateEntry(
|
|
||||||
Function<String, Boolean> isCommented,
|
|
||||||
Function<String, String> extractKey,
|
|
||||||
Function<String, Integer> getIndentationLevel,
|
|
||||||
List<String> userLines,
|
List<String> userLines,
|
||||||
String userLine,
|
String templateLine,
|
||||||
List<String> templateLines) {
|
String key,
|
||||||
String userKey = extractKey.apply(userLine).trim();
|
int position) {
|
||||||
int userIndentation = getIndentationLevel.apply(userLine);
|
boolean added = false;
|
||||||
|
int templateIndentationLevel = getIndentationLevel(templateLine);
|
||||||
// Start by assuming the line is not a child of an entry in templateLines
|
int pos = 0;
|
||||||
boolean isChild = false;
|
for (String settingsLine : userLines) {
|
||||||
|
if (settingsLine.trim().startsWith(key + ":") && position == pos) {
|
||||||
// Iterate backwards through userLines from the current line to find any parent
|
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
||||||
for (int i = userLines.indexOf(userLine) - 1; i >= 0; i--) {
|
// Check if it is correct settingsLine and has the same parent as templateLine
|
||||||
String potentialParentLine = userLines.get(i);
|
if (settingsIndentationLevel == templateIndentationLevel) {
|
||||||
int parentIndentation = getIndentationLevel.apply(potentialParentLine);
|
resultLines.add(settingsLine);
|
||||||
|
added = true;
|
||||||
// Check if we've reached a potential parent based on indentation
|
|
||||||
if (parentIndentation < userIndentation) {
|
|
||||||
String parentKey = extractKey.apply(potentialParentLine).trim();
|
|
||||||
|
|
||||||
// Now, check if this potential parent or any of its parents exist in templateLines
|
|
||||||
boolean parentExistsInTemplate =
|
|
||||||
templateLines.stream()
|
|
||||||
.filter(line -> !isCommented.apply(line)) // Skip commented lines
|
|
||||||
.anyMatch(
|
|
||||||
templateLine -> {
|
|
||||||
String templateKey =
|
|
||||||
extractKey.apply(templateLine).trim();
|
|
||||||
return parentKey.equalsIgnoreCase(templateKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!parentExistsInTemplate) {
|
|
||||||
// If the parent does not exist in template, check the next level parent
|
|
||||||
userIndentation =
|
|
||||||
parentIndentation; // Update userIndentation to the parent's indentation
|
|
||||||
// for next iteration
|
|
||||||
if (parentIndentation == 0) {
|
|
||||||
// If we've reached the top-level parent and it's not in template, the
|
|
||||||
// original line is considered not a child
|
|
||||||
isChild = false;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
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 {
|
} else {
|
||||||
// If any parent exists in template, the original line is considered a child
|
|
||||||
isChild = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return indentationLevel;
|
||||||
|
|
||||||
return isChild; // Return true if the line is not a child of any entry in templateLines
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,6 +116,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Security", "change-permissions");
|
addEndpointToGroup("Security", "change-permissions");
|
||||||
addEndpointToGroup("Security", "add-watermark");
|
addEndpointToGroup("Security", "add-watermark");
|
||||||
addEndpointToGroup("Security", "cert-sign");
|
addEndpointToGroup("Security", "cert-sign");
|
||||||
|
addEndpointToGroup("Security", "remove-cert-sign");
|
||||||
addEndpointToGroup("Security", "sanitize-pdf");
|
addEndpointToGroup("Security", "sanitize-pdf");
|
||||||
addEndpointToGroup("Security", "auto-redact");
|
addEndpointToGroup("Security", "auto-redact");
|
||||||
|
|
||||||
@ -200,6 +201,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "extract-images");
|
addEndpointToGroup("Java", "extract-images");
|
||||||
addEndpointToGroup("Java", "change-metadata");
|
addEndpointToGroup("Java", "change-metadata");
|
||||||
addEndpointToGroup("Java", "cert-sign");
|
addEndpointToGroup("Java", "cert-sign");
|
||||||
|
addEndpointToGroup("Java", "remove-cert-sign");
|
||||||
addEndpointToGroup("Java", "multi-page-layout");
|
addEndpointToGroup("Java", "multi-page-layout");
|
||||||
addEndpointToGroup("Java", "scale-pages");
|
addEndpointToGroup("Java", "scale-pages");
|
||||||
addEndpointToGroup("Java", "add-page-numbers");
|
addEndpointToGroup("Java", "add-page-numbers");
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
import org.thymeleaf.IEngineConfiguration;
|
import org.thymeleaf.IEngineConfiguration;
|
||||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||||
import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
|
|
||||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||||
import org.thymeleaf.templateresource.ITemplateResource;
|
import org.thymeleaf.templateresource.ITemplateResource;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.InputStreamTemplateResource;
|
||||||
|
|
||||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||||
|
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
@ -40,9 +42,13 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ClassLoaderTemplateResource(
|
InputStream inputStream =
|
||||||
Thread.currentThread().getContextClassLoader(),
|
Thread.currentThread()
|
||||||
"classpath:/templates/" + resourceName,
|
.getContextClassLoader()
|
||||||
characterEncoding);
|
.getResourceAsStream("templates/" + resourceName);
|
||||||
|
if (inputStream != null) {
|
||||||
|
return new InputStreamTemplateResource(inputStream, "UTF-8");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,6 @@ public class MetricsFilter extends OncePerRequestFilter {
|
|||||||
|| uri.startsWith("/v1/api-docs")
|
|| uri.startsWith("/v1/api-docs")
|
||||||
|| uri.endsWith("robots.txt")
|
|| uri.endsWith("robots.txt")
|
||||||
|| uri.startsWith("/images")
|
|| uri.startsWith("/images")
|
||||||
|| uri.startsWith("/images")
|
|
||||||
|| uri.endsWith(".png")
|
|| uri.endsWith(".png")
|
||||||
|| uri.endsWith(".ico")
|
|| uri.endsWith(".ico")
|
||||||
|| uri.endsWith(".css")
|
|| uri.endsWith(".css")
|
||||||
|
@ -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,27 +3,31 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.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;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Component
|
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Autowired private final LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired private final UserService userService; // Inject the UserService
|
private UserService userService;
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
||||||
|
|
||||||
public CustomAuthenticationFailureHandler(
|
public CustomAuthenticationFailureHandler(
|
||||||
LoginAttemptService loginAttemptService, UserService userService) {
|
final LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
@ -34,29 +38,43 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
logger.error("Failed login attempt from IP: " + ip);
|
logger.error("Failed login attempt from IP: {}", ip);
|
||||||
|
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
|
||||||
|
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
||||||
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
|
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String username = request.getParameter("username");
|
String username = request.getParameter("username");
|
||||||
if (!isDemoUser(username)) {
|
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
||||||
if (loginAttemptService.loginAttemptCheck(username)) {
|
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
|
||||||
|
|
||||||
} else {
|
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
||||||
if (exception.getClass().isAssignableFrom(LockedException.class)) {
|
logger.info(
|
||||||
setDefaultFailureUrl("/login?error=locked");
|
"Remaining attempts for user {}: {}",
|
||||||
|
optUser.get().getUsername(),
|
||||||
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
|
loginAttemptService.loginFailed(username);
|
||||||
|
if (loginAttemptService.isBlocked(username)
|
||||||
|
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
|
response.sendRedirect(contextPath + "/login?error=locked");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
|
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
||||||
setDefaultFailureUrl("/login?error=badcredentials");
|
response.sendRedirect(contextPath + "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDemoUser(String username) {
|
private boolean isDemoUser(Optional<User> user) {
|
||||||
Optional<User> user = userService.findByUsernameIgnoreCase(username);
|
|
||||||
return user.isPresent()
|
return user.isPresent()
|
||||||
&& user.get().getAuthorities().stream()
|
&& user.get().getAuthorities().stream()
|
||||||
.anyMatch(authority -> "ROLE_DEMO_USER".equals(authority.getAuthority()));
|
.anyMatch(authority -> "ROLE_DEMO_USER".equals(authority.getAuthority()));
|
||||||
|
@ -2,11 +2,9 @@ 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.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@ -14,25 +12,30 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
|
||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
@Autowired private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(
|
public void onAuthenticationSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
String username = request.getParameter("username");
|
|
||||||
loginAttemptService.loginSucceeded(username);
|
String userName = request.getParameter("username");
|
||||||
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
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(savedRequest.getRedirectUrl())) {
|
||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
|
@ -2,42 +2,32 @@ 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.session.SessionRegistry;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
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 jakarta.servlet.ServletException;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
|
||||||
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
{
|
|
||||||
@Bean
|
@Autowired SessionRegistry sessionRegistry;
|
||||||
public SessionRegistry sessionRegistry() {
|
|
||||||
return new SessionRegistryImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
|
public void onLogoutSuccess(
|
||||||
{
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
String sessionId = session.getId();
|
String sessionId = session.getId();
|
||||||
sessionRegistry()
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
.removeSessionInformation(
|
session.invalidate();
|
||||||
sessionId);
|
logger.debug("Session invalidated: " + sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(request.getParameter("oauth2AutoCreateDisabled") != null)
|
|
||||||
{
|
|
||||||
response.sendRedirect(request.getContextPath()+"/login?error=oauth2AutoCreateDisabled");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
@ -39,6 +39,10 @@ public class CustomUserDetailsService implements UserDetailsService {
|
|||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user.hasPassword()) {
|
||||||
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getPassword(),
|
user.getPassword(),
|
||||||
|
@ -44,7 +44,7 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
&& user.isPresent()
|
&& user.isPresent()
|
||||||
&& user.get().isFirstLogin()
|
&& user.get().isFirstLogin()
|
||||||
&& !"/change-creds".equals(requestURI)) {
|
&& !"/change-creds".equals(requestURI)) {
|
||||||
response.sendRedirect("/change-creds");
|
response.sendRedirect(request.getContextPath() + "/change-creds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ import java.nio.file.Paths;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -19,40 +21,67 @@ public class InitialSecuritySetup {
|
|||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class);
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
if (!userService.hasUsers()) {
|
if (!userService.hasUsers()) {
|
||||||
|
initializeAdminUser();
|
||||||
|
}
|
||||||
|
initializeInternalApiUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initSecretKey() throws IOException {
|
||||||
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
|
if (!isValidUUID(secretKey)) {
|
||||||
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
|
saveKeyToConfig(secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeAdminUser() {
|
||||||
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 && initialPassword != null) {
|
|
||||||
|
if (initialUsername != null
|
||||||
|
&& !initialUsername.isEmpty()
|
||||||
|
&& initialPassword != null
|
||||||
|
&& !initialPassword.isEmpty()
|
||||||
|
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
||||||
|
try {
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||||
|
logger.info("Admin user created: " + initialUsername);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.error("Failed to initialize security setup", e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
initialUsername = "admin";
|
createDefaultAdminUser();
|
||||||
initialPassword = "stirling";
|
|
||||||
userService.saveUser(
|
|
||||||
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createDefaultAdminUser() {
|
||||||
|
String defaultUsername = "admin";
|
||||||
|
String defaultPassword = "stirling";
|
||||||
|
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
||||||
|
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
||||||
|
logger.info("Default admin user created: " + defaultUsername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeInternalApiUser() {
|
||||||
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());
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initSecretKey() throws IOException {
|
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
|
||||||
if (secretKey == null || secretKey.isEmpty()) {
|
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
|
||||||
saveKeyToConfig(secretKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,4 +114,16 @@ public class InitialSecuritySetup {
|
|||||||
// Write back to the file
|
// Write back to the file
|
||||||
Files.write(path, lines);
|
Files.write(path, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isValidUUID(String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ 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;
|
||||||
|
|
||||||
@ -15,44 +17,61 @@ public class LoginAttemptService {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private int MAX_ATTEMPTS;
|
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
|
||||||
|
|
||||||
|
private int MAX_ATTEMPT;
|
||||||
private long ATTEMPT_INCREMENT_TIME;
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
|
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
|
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
ATTEMPT_INCREMENT_TIME =
|
ATTEMPT_INCREMENT_TIME =
|
||||||
TimeUnit.MINUTES.toMillis(
|
TimeUnit.MINUTES.toMillis(
|
||||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
|
attemptsCache = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public void loginSucceeded(String key) {
|
public void loginSucceeded(String key) {
|
||||||
attemptsCache.remove(key);
|
if (key == null || key.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attemptsCache.remove(key.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean loginAttemptCheck(String key) {
|
public void loginFailed(String key) {
|
||||||
attemptsCache.compute(
|
if (key == null || key.trim().isEmpty()) return;
|
||||||
key,
|
|
||||||
(k, attemptCounter) -> {
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null
|
if (attemptCounter == null) {
|
||||||
|| attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
attemptCounter = new AttemptCounter();
|
||||||
return new AttemptCounter();
|
attemptsCache.put(key.toLowerCase(), attemptCounter);
|
||||||
} else {
|
} else {
|
||||||
attemptCounter.increment();
|
if (attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
|
||||||
return attemptCounter;
|
attemptCounter.reset();
|
||||||
|
}
|
||||||
|
attemptCounter.increment();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlocked(String key) {
|
public boolean isBlocked(String key) {
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key);
|
if (key == null || key.trim().isEmpty()) return false;
|
||||||
if (attemptCounter != null) {
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
|
if (attemptCounter == null) {
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingAttempts(String key) {
|
||||||
|
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT;
|
||||||
|
|
||||||
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
|
if (attemptCounter == null) {
|
||||||
|
return MAX_ATTEMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MAX_ATTEMPT - attemptCounter.getAttemptCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import java.util.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
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.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@ -14,36 +15,45 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
|
|||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
||||||
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;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
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.Client;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity()
|
@EnableWebSecurity()
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private UserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
@ -92,7 +102,8 @@ public class SecurityConfiguration {
|
|||||||
formLogin
|
formLogin
|
||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler())
|
new CustomAuthenticationSuccessHandler(
|
||||||
|
loginAttemptService))
|
||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
@ -103,20 +114,9 @@ public class SecurityConfiguration {
|
|||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(
|
logout.logoutRequestMatcher(
|
||||||
new AntPathRequestMatcher("/logout"))
|
new AntPathRequestMatcher("/logout"))
|
||||||
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) // Use a Custom Logout Handler to handle custom error message if OAUTH2 Auto Create is disabled
|
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
.deleteCookies("JSESSIONID", "remember-me")
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
.addLogoutHandler(
|
|
||||||
(request, response, authentication) -> {
|
|
||||||
HttpSession session =
|
|
||||||
request.getSession(false);
|
|
||||||
if (session != null) {
|
|
||||||
String sessionId = session.getId();
|
|
||||||
sessionRegistry()
|
|
||||||
.removeSessionInformation(
|
|
||||||
sessionId);
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.rememberMe(
|
.rememberMe(
|
||||||
rememberMeConfigurer ->
|
rememberMeConfigurer ->
|
||||||
rememberMeConfigurer // Use the configurator directly
|
rememberMeConfigurer // Use the configurator directly
|
||||||
@ -148,6 +148,7 @@ public class SecurityConfiguration {
|
|||||||
|| trimmedUri.startsWith("/images/")
|
|| trimmedUri.startsWith("/images/")
|
||||||
|| trimmedUri.startsWith("/public/")
|
|| trimmedUri.startsWith("/public/")
|
||||||
|| trimmedUri.startsWith("/css/")
|
|| trimmedUri.startsWith("/css/")
|
||||||
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|| trimmedUri.startsWith("/js/")
|
|| trimmedUri.startsWith("/js/")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/api/v1/info/status");
|
"/api/v1/info/status");
|
||||||
@ -155,34 +156,45 @@ public class SecurityConfiguration {
|
|||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated())
|
.authenticated())
|
||||||
.userDetailsService(userDetailsService)
|
|
||||||
.authenticationProvider(authenticationProvider());
|
.authenticationProvider(authenticationProvider());
|
||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
if (applicationProperties.getSecurity().getOAUTH2() != null
|
||||||
|
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||||
|
|
||||||
http.oauth2Login( oauth2 -> oauth2
|
http.oauth2Login(
|
||||||
.loginPage("/oauth2")
|
oauth2 ->
|
||||||
|
oauth2.loginPage("/oauth2")
|
||||||
/*
|
/*
|
||||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||||
is set as true, else login fails with an error message advising the same.
|
is set as true, else login fails with an error message advising the same.
|
||||||
*/
|
*/
|
||||||
.successHandler(new AuthenticationSuccessHandler() {
|
.successHandler(
|
||||||
@Override
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
|
loginAttemptService,
|
||||||
Authentication authentication) throws ServletException , IOException{
|
applicationProperties,
|
||||||
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
userService))
|
||||||
if (userService.processOAuth2PostLogin(oauthUser.getAttribute("email"), applicationProperties.getSecurity().getOAUTH2().getAutoCreateUser())) {
|
.failureHandler(
|
||||||
response.sendRedirect("/");
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
}
|
// Add existing Authorities from the database
|
||||||
else{
|
.userInfoEndpoint(
|
||||||
response.sendRedirect("/logout?oauth2AutoCreateDisabled=true");
|
userInfoEndpoint ->
|
||||||
}
|
userInfoEndpoint
|
||||||
}
|
.oidcUserService(
|
||||||
}
|
new CustomOAuth2UserService(
|
||||||
)
|
applicationProperties,
|
||||||
);
|
userService,
|
||||||
|
loginAttemptService))
|
||||||
|
.userAuthoritiesMapper(
|
||||||
|
userAuthoritiesMapper())))
|
||||||
|
.logout(
|
||||||
|
logout ->
|
||||||
|
logout.logoutSuccessHandler(
|
||||||
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
|
this.applicationProperties,
|
||||||
|
sessionRegistry()))
|
||||||
|
.invalidateHttpSession(true));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
@ -194,20 +206,174 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
// Client Registration Repository for OAUTH2 OIDC Login
|
// Client Registration Repository for OAUTH2 OIDC Login
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(value = "security.oauth2.enabled" , havingValue = "true", matchIfMissing = false)
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||||||
|
|
||||||
|
githubClientRegistration().ifPresent(registrations::add);
|
||||||
|
oidcClientRegistration().ifPresent(registrations::add);
|
||||||
|
googleClientRegistration().ifPresent(registrations::add);
|
||||||
|
keycloakClientRegistration().ifPresent(registrations::add);
|
||||||
|
|
||||||
|
if (registrations.isEmpty()) {
|
||||||
|
logger.error("At least one OAuth2 provider must be configured");
|
||||||
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRegistration oidcClientRegistration() {
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
return ClientRegistrations.fromOidcIssuerLocation(applicationProperties.getSecurity().getOAUTH2().getIssuer())
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> googleClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
return google != null && google.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistration.withRegistrationId(google.getName())
|
||||||
|
.clientId(google.getClientId())
|
||||||
|
.clientSecret(google.getClientSecret())
|
||||||
|
.scope(google.getScopes())
|
||||||
|
.authorizationUri(google.getAuthorizationuri())
|
||||||
|
.tokenUri(google.getTokenuri())
|
||||||
|
.userInfoUri(google.getUserinfouri())
|
||||||
|
.userNameAttributeName(google.getUseAsUsername())
|
||||||
|
.clientName(google.getClientName())
|
||||||
|
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
||||||
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> keycloakClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
|
|
||||||
|
return keycloak != null && keycloak.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||||
|
.registrationId(keycloak.getName())
|
||||||
|
.clientId(keycloak.getClientId())
|
||||||
|
.clientSecret(keycloak.getClientSecret())
|
||||||
|
.scope(keycloak.getScopes())
|
||||||
|
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||||
|
.clientName(keycloak.getClientName())
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GithubProvider github = client.getGithub();
|
||||||
|
return github != null && github.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistration.withRegistrationId(github.getName())
|
||||||
|
.clientId(github.getClientId())
|
||||||
|
.clientSecret(github.getClientSecret())
|
||||||
|
.scope(github.getScopes())
|
||||||
|
.authorizationUri(github.getAuthorizationuri())
|
||||||
|
.tokenUri(github.getTokenuri())
|
||||||
|
.userInfoUri(github.getUserinfouri())
|
||||||
|
.userNameAttributeName(github.getUseAsUsername())
|
||||||
|
.clientName(github.getClientName())
|
||||||
|
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
||||||
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> oidcClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null
|
||||||
|
|| oauth.getIssuer() == null
|
||||||
|
|| oauth.getIssuer().isEmpty()
|
||||||
|
|| oauth.getClientId() == null
|
||||||
|
|| oauth.getClientId().isEmpty()
|
||||||
|
|| oauth.getClientSecret() == null
|
||||||
|
|| oauth.getClientSecret().isEmpty()
|
||||||
|
|| oauth.getScopes() == null
|
||||||
|
|| oauth.getScopes().isEmpty()
|
||||||
|
|| oauth.getUseAsUsername() == null
|
||||||
|
|| oauth.getUseAsUsername().isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(
|
||||||
|
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||||
.registrationId("oidc")
|
.registrationId("oidc")
|
||||||
.clientId(applicationProperties.getSecurity().getOAUTH2().getClientId())
|
.clientId(oauth.getClientId())
|
||||||
.clientSecret(applicationProperties.getSecurity().getOAUTH2().getClientSecret())
|
.clientSecret(oauth.getClientSecret())
|
||||||
.scope("openid", "profile", "email")
|
.scope(oauth.getScopes())
|
||||||
.userNameAttributeName("email")
|
.userNameAttributeName(oauth.getUseAsUsername())
|
||||||
.clientName("OIDC")
|
.clientName("OIDC")
|
||||||
.build();
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
|
return (authorities) -> {
|
||||||
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
|
authorities.forEach(
|
||||||
|
authority -> {
|
||||||
|
// Add existing OAUTH2 Authorities
|
||||||
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
|
|
||||||
|
// Add Authorities from database for existing user, if user is present.
|
||||||
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
||||||
|
String useAsUsername =
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getOAUTH2()
|
||||||
|
.getUseAsUsername();
|
||||||
|
Optional<User> userOpt =
|
||||||
|
userService.findByUsernameIgnoreCase(
|
||||||
|
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
if (user != null) {
|
||||||
|
mappedAuthorities.add(
|
||||||
|
new SimpleGrantedAuthority(
|
||||||
|
userService.findRole(user).getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mappedAuthorities;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -101,6 +101,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
contextPath + "/images/",
|
contextPath + "/images/",
|
||||||
contextPath + "/public/",
|
contextPath + "/public/",
|
||||||
contextPath + "/css/",
|
contextPath + "/css/",
|
||||||
|
contextPath + "/fonts/",
|
||||||
contextPath + "/js/",
|
contextPath + "/js/",
|
||||||
contextPath + "/pdfjs/",
|
contextPath + "/pdfjs/",
|
||||||
contextPath + "/api/v1/info/status",
|
contextPath + "/api/v1/info/status",
|
||||||
|
@ -20,6 +20,7 @@ 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.bucket4j.Refill;
|
||||||
|
import io.github.pixee.security.Newlines;
|
||||||
|
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@ -125,12 +126,16 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
|
||||||
|
|
||||||
if (probe.isConsumed()) {
|
if (probe.isConsumed()) {
|
||||||
response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
|
response.setHeader(
|
||||||
|
"X-Rate-Limit-Remaining",
|
||||||
|
Newlines.stripAll(Long.toString(probe.getRemainingTokens())));
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
} else {
|
} else {
|
||||||
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
|
||||||
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
|
||||||
response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
|
response.setHeader(
|
||||||
|
"X-Rate-Limit-Retry-After-Seconds",
|
||||||
|
Newlines.stripAll(String.valueOf(waitForRefill)));
|
||||||
response.getWriter().write("Rate limit exceeded for POST requests.");
|
response.getWriter().write("Rate limit exceeded for POST requests.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ import java.util.UUID;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.MessageSource;
|
||||||
|
import org.springframework.context.i18n.LocaleContextHolder;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
@ -18,9 +20,11 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
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.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
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.repository.AuthorityRepository;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@ -28,21 +32,23 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired private UserRepository userRepository;
|
@Autowired private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired private AuthorityRepository authorityRepository;
|
||||||
|
|
||||||
@Autowired private PasswordEncoder passwordEncoder;
|
@Autowired private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired private MessageSource messageSource;
|
||||||
|
|
||||||
// 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) {
|
||||||
Optional<User> existUser = userRepository.findByUsernameIgnoreCase(username);
|
if (!isUsernameValid(username)) {
|
||||||
if (existUser.isPresent()) {
|
return false;
|
||||||
|
}
|
||||||
|
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
if (existingUser.isPresent()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (autoCreateUser) {
|
if (autoCreateUser) {
|
||||||
User user = new User();
|
saveUser(username, AuthenticationType.OAUTH2);
|
||||||
user.setUsername(username);
|
|
||||||
user.setEnabled(true);
|
|
||||||
user.setFirstLogin(false);
|
|
||||||
user.addAuthority(new Authority( Role.USER.getRoleId(), user));
|
|
||||||
userRepository.save(user);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -108,9 +114,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
User userOptional = userRepository.findByApiKey(apiKey);
|
User user = userRepository.findByApiKey(apiKey);
|
||||||
if (userOptional != null) {
|
if (user != null) {
|
||||||
User user = userOptional;
|
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return new org.springframework.security.core.userdetails.User(
|
||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
@ -122,35 +127,53 @@ 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 = userRepository.findByUsernameIgnoreCase(username);
|
||||||
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password) {
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(false);
|
||||||
|
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
||||||
|
user.setAuthenticationType(authenticationType);
|
||||||
|
userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, String password) throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role, boolean firstLogin) {
|
public void saveUser(String username, String password, String role, boolean firstLogin)
|
||||||
|
throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(username)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
user.addAuthority(new Authority(role, user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
user.setFirstLogin(firstLogin);
|
user.setFirstLogin(firstLogin);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role) {
|
public void saveUser(String username, String password, String role)
|
||||||
User user = new User();
|
throws IllegalArgumentException {
|
||||||
user.setUsername(username);
|
saveUser(username, password, role, false);
|
||||||
user.setPassword(passwordEncoder.encode(password));
|
|
||||||
user.addAuthority(new Authority(role, user));
|
|
||||||
user.setEnabled(true);
|
|
||||||
user.setFirstLogin(false);
|
|
||||||
userRepository.save(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUser(String username) {
|
public void deleteUser(String username) {
|
||||||
@ -174,7 +197,13 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasUsers() {
|
public boolean hasUsers() {
|
||||||
return userRepository.count() > 0;
|
long userCount = userRepository.count();
|
||||||
|
if (userRepository
|
||||||
|
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
|
||||||
|
.isPresent()) {
|
||||||
|
userCount -= 1;
|
||||||
|
}
|
||||||
|
return userCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateUserSettings(String username, Map<String, String> updates) {
|
public void updateUserSettings(String username, Map<String, String> updates) {
|
||||||
@ -184,7 +213,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
Map<String, String> settingsMap = user.getSettings();
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
|
|
||||||
if (settingsMap == null) {
|
if (settingsMap == null) {
|
||||||
settingsMap = new HashMap<String, String>();
|
settingsMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
settingsMap.clear();
|
settingsMap.clear();
|
||||||
settingsMap.putAll(updates);
|
settingsMap.putAll(updates);
|
||||||
@ -202,7 +231,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
return userRepository.findByUsernameIgnoreCase(username);
|
return userRepository.findByUsernameIgnoreCase(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeUsername(User user, String newUsername) {
|
public Authority findRole(User user) {
|
||||||
|
return authorityRepository.findByUserId(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeUsername(User user, String newUsername) throws IllegalArgumentException {
|
||||||
|
if (!isUsernameValid(newUsername)) {
|
||||||
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
|
}
|
||||||
user.setUsername(newUsername);
|
user.setUsername(newUsername);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
@ -217,11 +253,41 @@ public class UserService implements UserServiceInterface {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeRole(User user, String newRole) {
|
||||||
|
Authority userAuthority = this.findRole(user);
|
||||||
|
userAuthority.setAuthority(newRole);
|
||||||
|
authorityRepository.save(userAuthority);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
return passwordEncoder.matches(currentPassword, user.getPassword());
|
return passwordEncoder.matches(currentPassword, user.getPassword());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUsernameValid(String username) {
|
public boolean isUsernameValid(String username) {
|
||||||
return username.matches("[a-zA-Z0-9]+");
|
// Checks whether the simple username is formatted correctly
|
||||||
|
boolean isValidSimpleUsername =
|
||||||
|
username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$");
|
||||||
|
// Checks whether the email address is formatted correctly
|
||||||
|
boolean isValidEmail =
|
||||||
|
username.matches(
|
||||||
|
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
||||||
|
return isValidSimpleUsername || isValidEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getInvalidUsernameMessage() {
|
||||||
|
return messageSource.getMessage(
|
||||||
|
"invalidUsernameMessage", null, LocaleContextHolder.getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasPassword(String username) {
|
||||||
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
return user.isPresent() && user.get().hasPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAuthenticationTypeByUsername(
|
||||||
|
String username, AuthenticationType authenticationType) {
|
||||||
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
|
return user.isPresent()
|
||||||
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationException exception)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
|
String errorCode = error.getErrorCode();
|
||||||
|
|
||||||
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
|
errorCode = "userAlreadyExistsWeb";
|
||||||
|
}
|
||||||
|
logger.error("OAuth2 Authentication error: " + errorCode);
|
||||||
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
|
||||||
|
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
public CustomOAuth2AuthenticationSuccessHandler(
|
||||||
|
final LoginAttemptService loginAttemptService,
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserService userService) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
// Get the saved request
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
SavedRequest savedRequest =
|
||||||
|
(session != null)
|
||||||
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (savedRequest != null
|
||||||
|
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
|
||||||
|
// Redirect to the original destination
|
||||||
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
} else {
|
||||||
|
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
||||||
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
|
String username = oauthUser.getName();
|
||||||
|
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
if (session != null) {
|
||||||
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
|
}
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
|
&& userService.hasPassword(username)
|
||||||
|
&& !userService.isAuthenticationTypeByUsername(
|
||||||
|
username, AuthenticationType.OAUTH2)
|
||||||
|
&& oAuth.getAutoCreateUser()) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
response.sendRedirect("/");
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.sendRedirect("/logout?invalidUsername=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,122 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.Provider;
|
||||||
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
|
||||||
|
|
||||||
|
private final SessionRegistry sessionRegistry;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public CustomOAuth2LogoutSuccessHandler(
|
||||||
|
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
String param = "logout=true";
|
||||||
|
String registrationId = null;
|
||||||
|
String issuer = null;
|
||||||
|
String clientId = null;
|
||||||
|
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
|
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
||||||
|
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Provider provider = oauth.getClient().get(registrationId);
|
||||||
|
issuer = provider.getIssuer();
|
||||||
|
clientId = provider.getClientId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("exception", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
|
issuer = oauth.getIssuer();
|
||||||
|
clientId = oauth.getClientId();
|
||||||
|
}
|
||||||
|
String errorMessage = "";
|
||||||
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
|
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||||
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
|
param = "error=" + sanitizeInput(errorMessage);
|
||||||
|
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
||||||
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
case "keycloak":
|
||||||
|
// Add Keycloak specific logout URL if needed
|
||||||
|
String logoutUrl =
|
||||||
|
issuer
|
||||||
|
+ "/protocol/openid-connect/logout"
|
||||||
|
+ "?client_id="
|
||||||
|
+ clientId
|
||||||
|
+ "&post_logout_redirect_uri="
|
||||||
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
|
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
|
response.sendRedirect(logoutUrl);
|
||||||
|
break;
|
||||||
|
case "github":
|
||||||
|
// Add GitHub specific logout URL if needed
|
||||||
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
|
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
|
response.sendRedirect(githubLogoutUrl);
|
||||||
|
break;
|
||||||
|
case "google":
|
||||||
|
// Add Google specific logout URL if needed
|
||||||
|
// String googleLogoutUrl =
|
||||||
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
|
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
String redirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
|
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
||||||
|
response.sendRedirect(redirectUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeInput(String input) {
|
||||||
|
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||||
|
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||||
|
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
||||||
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
|
|
||||||
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
|
||||||
|
|
||||||
|
public CustomOAuth2UserService(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
UserService userService,
|
||||||
|
LoginAttemptService loginAttemptService) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.userService = userService;
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
|
OAUTH2 oauth2 = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
String usernameAttribute = oauth2.getUseAsUsername();
|
||||||
|
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
||||||
|
Client client = oauth2.getClient();
|
||||||
|
if (client != null && client.getKeycloak() != null) {
|
||||||
|
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
||||||
|
} else {
|
||||||
|
usernameAttribute = "email";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
|
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
||||||
|
|
||||||
|
// Check if the username claim is null or empty
|
||||||
|
if (username == null || username.trim().isEmpty()) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Claim '" + usernameAttribute + "' cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
if (duser.isPresent()) {
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
if (userService.hasPassword(username)) {
|
||||||
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a new OidcUser with adjusted attributes
|
||||||
|
return new DefaultOidcUser(
|
||||||
|
user.getAuthorities(),
|
||||||
|
userRequest.getIdToken(),
|
||||||
|
user.getUserInfo(),
|
||||||
|
usernameAttribute);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
logger.error("Error loading OIDC user: {}", e.getMessage());
|
||||||
|
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error loading OIDC user", e);
|
||||||
|
throw new OAuth2AuthenticationException("Unexpected error during authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,34 +115,77 @@ 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) {
|
||||||
file.delete();
|
if (file != null) {
|
||||||
|
Files.deleteIfExists(file.toPath()); // Delete temporary files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
docOutputstream.close();
|
||||||
|
if (mergedDocument != null) {
|
||||||
|
mergedDocument.close(); // Close the merged document
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,12 +87,12 @@ public class PdfOverlayController {
|
|||||||
} finally {
|
} finally {
|
||||||
for (File overlayPdfFile : overlayPdfFiles) {
|
for (File overlayPdfFile : overlayPdfFiles) {
|
||||||
if (overlayPdfFile != null) {
|
if (overlayPdfFile != null) {
|
||||||
overlayPdfFile.delete();
|
Files.deleteIfExists(overlayPdfFile.toPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (File tempFile : tempFiles) { // Delete temporary files
|
for (File tempFile : tempFiles) { // Delete temporary files
|
||||||
if (tempFile != null) {
|
if (tempFile != null) {
|
||||||
tempFile.delete();
|
Files.deleteIfExists(tempFile.toPath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +121,7 @@ public class SplitPDFController {
|
|||||||
|
|
||||||
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
|
||||||
byte[] data = Files.readAllBytes(zipFile);
|
byte[] data = Files.readAllBytes(zipFile);
|
||||||
Files.delete(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
|
|
||||||
// return the Resource in the response
|
// return the Resource in the response
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
@ -18,6 +18,8 @@ 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.form.PDFormXObject;
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
import org.apache.pdfbox.util.Matrix;
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
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;
|
||||||
@ -38,6 +40,9 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class SplitPdfBySectionsController {
|
public class SplitPdfBySectionsController {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(SplitPdfBySectionsController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Split PDF pages into smaller sections",
|
summary = "Split PDF pages into smaller sections",
|
||||||
@ -63,10 +68,7 @@ public class SplitPdfBySectionsController {
|
|||||||
MergeController mergeController = new MergeController();
|
MergeController mergeController = new MergeController();
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
mergeController.mergeDocuments(splitDocuments).save(baos);
|
mergeController.mergeDocuments(splitDocuments).save(baos);
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), filename + "_split.pdf");
|
||||||
baos.toByteArray(),
|
|
||||||
filename + "_split.pdf",
|
|
||||||
MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
}
|
}
|
||||||
for (PDDocument doc : splitDocuments) {
|
for (PDDocument doc : splitDocuments) {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
@ -95,10 +97,10 @@ public class SplitPdfBySectionsController {
|
|||||||
if (sectionNum == horiz * verti) pageNum++;
|
if (sectionNum == horiz * verti) pageNum++;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
} finally {
|
} finally {
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
Files.delete(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
@ -10,6 +10,8 @@ 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;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
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;
|
||||||
@ -31,6 +33,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class SplitPdfBySizeController {
|
public class SplitPdfBySizeController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SplitPdfBySizeController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||||
@ -66,7 +70,7 @@ public class SplitPdfBySizeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
} finally {
|
} finally {
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
Files.deleteIfExists(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
|
@ -47,8 +47,11 @@ public class UserController {
|
|||||||
model.addAttribute("error", "Username already exists");
|
model.addAttribute("error", "Username already exists");
|
||||||
return "register";
|
return "register";
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
|
userService.saveUser(requestModel.getUsername(), requestModel.getPassword());
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return "redirect:/login?messageType=invalidUsername";
|
||||||
|
}
|
||||||
return "redirect:/login?registered=true";
|
return "redirect:/login?registered=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,42 +66,46 @@ public class UserController {
|
|||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes) {
|
||||||
|
|
||||||
if (!userService.isUsernameValid(newUsername)) {
|
if (!userService.isUsernameValid(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=invalidUsername");
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated");
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The username MUST be unique when renaming
|
// The username MUST be unique when renaming
|
||||||
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
Optional<User> userOpt = userService.findByUsername(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound");
|
return new RedirectView("/account?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (user.getUsername().equals(newUsername)) {
|
if (user.getUsername().equals(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=usernameExists");
|
return new RedirectView("/account?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword");
|
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=usernameExists");
|
return new RedirectView("/account?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newUsername != null && newUsername.length() > 0) {
|
if (newUsername != null && newUsername.length() > 0) {
|
||||||
|
try {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@ -111,19 +118,19 @@ public class UserController {
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/change-creds?messageType=notAuthenticated");
|
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/change-creds?messageType=userNotFound");
|
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/change-creds?messageType=incorrectPassword");
|
return new RedirectView("/change-creds?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
@ -131,7 +138,7 @@ public class UserController {
|
|||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@ -144,19 +151,19 @@ public class UserController {
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes) {
|
RedirectAttributes redirectAttributes) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated");
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
|
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt == null || userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound");
|
return new RedirectView("/account?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
if (!userService.isPasswordCorrect(user, currentPassword)) {
|
||||||
return new RedirectView("/account?messageType=incorrectPassword");
|
return new RedirectView("/account?messageType=incorrectPassword", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.changePassword(user, newPassword);
|
userService.changePassword(user, newPassword);
|
||||||
@ -164,7 +171,7 @@ public class UserController {
|
|||||||
// Logout using Spring's utility
|
// Logout using Spring's utility
|
||||||
new SecurityContextLogoutHandler().logout(request, response, null);
|
new SecurityContextLogoutHandler().logout(request, response, null);
|
||||||
|
|
||||||
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED);
|
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@ -197,7 +204,7 @@ public class UserController {
|
|||||||
boolean forceChange) {
|
boolean forceChange) {
|
||||||
|
|
||||||
if (!userService.isUsernameValid(username)) {
|
if (!userService.isUsernameValid(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=invalidUsername");
|
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
@ -205,26 +212,67 @@ public class UserController {
|
|||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (userService.usernameExistsIgnoreCase(username)) {
|
if (userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists");
|
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Validate the role
|
// Validate the role
|
||||||
Role roleEnum = Role.fromString(role);
|
Role roleEnum = Role.fromString(role);
|
||||||
if (roleEnum == Role.INTERNAL_API_USER) {
|
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||||
// If the role is INTERNAL_API_USER, reject the request
|
// If the role is INTERNAL_API_USER, reject the request
|
||||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
// If the role ID is not valid, redirect with an error message
|
// If the role ID is not valid, redirect with an error message
|
||||||
return new RedirectView("/addUsers?messageType=invalidRole");
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.saveUser(username, password, role, forceChange);
|
userService.saveUser(username, password, role, forceChange);
|
||||||
return new RedirectView("/addUsers"); // Redirect to account page after adding the user
|
return new RedirectView(
|
||||||
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/changeRole")
|
||||||
|
public RedirectView changeRole(
|
||||||
|
@RequestParam(name = "username") String username,
|
||||||
|
@RequestParam(name = "role") String role,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
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=downgradeCurrentUser", true);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// Validate the role
|
||||||
|
Role roleEnum = Role.fromString(role);
|
||||||
|
if (roleEnum == Role.INTERNAL_API_USER) {
|
||||||
|
// If the role is INTERNAL_API_USER, reject the request
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// If the role ID is not valid, redirect with an error message
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
|
}
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
userService.changeRole(user, role);
|
||||||
|
return new RedirectView(
|
||||||
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@ -233,7 +281,7 @@ public class UserController {
|
|||||||
@PathVariable(name = "username") String username, Authentication authentication) {
|
@PathVariable(name = "username") String username, Authentication authentication) {
|
||||||
|
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists");
|
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
@ -241,11 +289,11 @@ public class UserController {
|
|||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser");
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||||
}
|
}
|
||||||
invalidateUserSessions(username);
|
invalidateUserSessions(username);
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return new RedirectView("/addUsers");
|
return new RedirectView("/addUsers", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired private SessionRegistry sessionRegistry;
|
@Autowired private SessionRegistry sessionRegistry;
|
||||||
|
@ -36,7 +36,7 @@ public class ConvertImgPDFController {
|
|||||||
description =
|
description =
|
||||||
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
|
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
|
||||||
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
|
||||||
throws IOException {
|
throws NumberFormatException, Exception {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String imageFormat = request.getImageFormat();
|
String imageFormat = request.getImageFormat();
|
||||||
String singleOrMultiple = request.getSingleOrMultiple();
|
String singleOrMultiple = request.getSingleOrMultiple();
|
||||||
@ -56,7 +56,7 @@ public class ConvertImgPDFController {
|
|||||||
String filename =
|
String filename =
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "");
|
||||||
try {
|
|
||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
@ -65,14 +65,10 @@ public class ConvertImgPDFController {
|
|||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
filename);
|
filename);
|
||||||
} catch (IOException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (result == null || result.length == 0) {
|
||||||
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
|
}
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
String docName = filename + "." + imageFormat;
|
String docName = filename + "." + imageFormat;
|
||||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||||
|
@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api.converters;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -41,11 +40,12 @@ public class ConvertOfficeController {
|
|||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile =
|
Path tempInputFile =
|
||||||
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename));
|
||||||
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
inputFile.transferTo(tempInputFile);
|
||||||
|
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
|
try {
|
||||||
// Run the LibreOffice command
|
// Run the LibreOffice command
|
||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
@ -63,12 +63,12 @@ public class ConvertOfficeController {
|
|||||||
|
|
||||||
// Read the converted PDF file
|
// Read the converted PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempInputFile);
|
|
||||||
Files.delete(tempOutputFile);
|
|
||||||
|
|
||||||
return pdfBytes;
|
return pdfBytes;
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidFileExtension(String fileExtension) {
|
private boolean isValidFileExtension(String fileExtension) {
|
||||||
|
@ -1,10 +1,22 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
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.LoggerFactory;
|
||||||
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;
|
||||||
@ -26,6 +38,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConvertPDFToPDFA {
|
public class ConvertPDFToPDFA {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
@ -36,9 +50,39 @@ public class ConvertPDFToPDFA {
|
|||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
String outputFormat = request.getOutputFormat();
|
String outputFormat = request.getOutputFormat();
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
// Convert MultipartFile to byte[]
|
||||||
|
byte[] pdfBytes = inputFile.getBytes();
|
||||||
|
|
||||||
|
// Load the PDF document
|
||||||
|
PDDocument document = Loader.loadPDF(pdfBytes);
|
||||||
|
|
||||||
|
// Get the document catalog
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
|
||||||
|
// Get the AcroForm
|
||||||
|
PDAcroForm acroForm = catalog.getAcroForm();
|
||||||
|
if (acroForm != null) {
|
||||||
|
// Remove signature fields safely
|
||||||
|
List<PDField> fieldsToRemove =
|
||||||
|
acroForm.getFields().stream()
|
||||||
|
.filter(field -> field instanceof PDSignatureField)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
|
acroForm.flatten(fieldsToRemove, false);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
pdfBytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
// Save the uploaded (and possibly modified) file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
||||||
|
outputStream.write(pdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
@ -58,17 +102,17 @@ public class ConvertPDFToPDFA {
|
|||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] optimizedPdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.delete(tempInputFile);
|
Files.deleteIfExists(tempInputFile);
|
||||||
Files.delete(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_PDFA.pdf";
|
+ "_PDFA.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(optimizedPdfBytes, outputFilename);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.delete(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
}
|
}
|
||||||
// Convert URL to a safe filename
|
// Convert URL to a safe filename
|
||||||
String outputFilename = convertURLToFileName(URL);
|
String outputFilename = convertURLToFileName(URL);
|
||||||
|
@ -15,6 +15,8 @@ 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;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
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;
|
||||||
@ -43,6 +45,7 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class AutoSplitPdfController {
|
public class AutoSplitPdfController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AutoSplitPdfController.class);
|
||||||
private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
|
private static final String QR_CONTENT = "https://github.com/Stirling-Tools/Stirling-PDF";
|
||||||
private static final String QR_CONTENT_OLD = "https://github.com/Frooodle/Stirling-PDF";
|
private static final String QR_CONTENT_OLD = "https://github.com/Frooodle/Stirling-PDF";
|
||||||
|
|
||||||
@ -115,10 +118,10 @@ public class AutoSplitPdfController {
|
|||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
} finally {
|
} finally {
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
Files.delete(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
@ -67,7 +67,7 @@ public class BlankPageController {
|
|||||||
String pageText = textStripper.getText(document);
|
String pageText = textStripper.getText(document);
|
||||||
boolean hasText = !pageText.trim().isEmpty();
|
boolean hasText = !pageText.trim().isEmpty();
|
||||||
|
|
||||||
Boolean blank = false;
|
Boolean blank = true;
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
logger.info("page " + pageIndex + " has text, not blank");
|
logger.info("page " + pageIndex + " has text, not blank");
|
||||||
blank = false;
|
blank = false;
|
||||||
@ -106,7 +106,7 @@ public class BlankPageController {
|
|||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_blanksRemoved.pdf");
|
+ "_blanksRemoved.pdf");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
} finally {
|
} finally {
|
||||||
if (document != null) document.close();
|
if (document != null) document.close();
|
||||||
|
@ -136,10 +136,10 @@ public class CompressController {
|
|||||||
// Increase optimization level for next iteration
|
// Increase optimization level for next iteration
|
||||||
optimizeLevel++;
|
optimizeLevel++;
|
||||||
if (autoMode && optimizeLevel > 4) {
|
if (autoMode && optimizeLevel > 4) {
|
||||||
System.out.println("Skipping level 5 due to bad results in auto mode");
|
logger.info("Skipping level 5 due to bad results in auto mode");
|
||||||
sizeMet = true;
|
sizeMet = true;
|
||||||
} else {
|
} else {
|
||||||
System.out.println(
|
logger.info(
|
||||||
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,10 +230,10 @@ public class CompressController {
|
|||||||
if (currentSize > expectedOutputSize) {
|
if (currentSize > expectedOutputSize) {
|
||||||
// Log the current file size and scaleFactor
|
// Log the current file size and scaleFactor
|
||||||
|
|
||||||
System.out.println(
|
logger.info(
|
||||||
"Current file size: "
|
"Current file size: "
|
||||||
+ FileUtils.byteCountToDisplaySize(currentSize));
|
+ FileUtils.byteCountToDisplaySize(currentSize));
|
||||||
System.out.println("Current scale factor: " + scaleFactor);
|
logger.info("Current scale factor: " + scaleFactor);
|
||||||
|
|
||||||
// The file is still too large, reduce scaleFactor and try again
|
// The file is still too large, reduce scaleFactor and try again
|
||||||
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
|
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
|
||||||
@ -256,7 +256,6 @@ public class CompressController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
@ -269,17 +268,18 @@ public class CompressController {
|
|||||||
// Read the original file again
|
// Read the original file again
|
||||||
pdfBytes = Files.readAllBytes(tempInputFile);
|
pdfBytes = Files.readAllBytes(tempInputFile);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempInputFile);
|
|
||||||
Files.delete(tempOutputFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_Optimized.pdf";
|
+ "_Optimized.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
// deleted by multipart file handler deu to transferTo?
|
||||||
|
// Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -103,10 +102,7 @@ public class ExtractImageScansController {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tempInputFile = Files.createTempFile("input_", "." + extension);
|
tempInputFile = Files.createTempFile("input_", "." + extension);
|
||||||
Files.copy(
|
form.getFileInput().transferTo(tempInputFile);
|
||||||
form.getFileInput().getInputStream(),
|
|
||||||
tempInputFile,
|
|
||||||
StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
// Add input file path to images list
|
// Add input file path to images list
|
||||||
images.add(tempInputFile.toString());
|
images.add(tempInputFile.toString());
|
||||||
}
|
}
|
||||||
@ -176,11 +172,15 @@ public class ExtractImageScansController {
|
|||||||
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||||
|
|
||||||
// Clean up the temporary zip file
|
// Clean up the temporary zip file
|
||||||
Files.delete(tempZipFile);
|
Files.deleteIfExists(tempZipFile);
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
if (processedImageBytes.size() == 0) {
|
||||||
|
throw new IllegalArgumentException("No images detected");
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Return the processed image as a response
|
// Return the processed image as a response
|
||||||
byte[] imageBytes = processedImageBytes.get(0);
|
byte[] imageBytes = processedImageBytes.get(0);
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
@ -201,7 +201,7 @@ public class ExtractImageScansController {
|
|||||||
|
|
||||||
if (tempZipFile != null && Files.exists(tempZipFile)) {
|
if (tempZipFile != null && Files.exists(tempZipFile)) {
|
||||||
try {
|
try {
|
||||||
Files.delete(tempZipFile);
|
Files.deleteIfExists(tempZipFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Failed to delete temporary zip file: " + tempZipFile, e);
|
logger.error("Failed to delete temporary zip file: " + tempZipFile, e);
|
||||||
}
|
}
|
||||||
|
@ -110,8 +110,8 @@ public class FakeScanControllerWIP {
|
|||||||
private BufferedImage rotate(BufferedImage image, double rotation) {
|
private BufferedImage rotate(BufferedImage image, double rotation) {
|
||||||
|
|
||||||
double rotationRequired = Math.toRadians(rotation);
|
double rotationRequired = Math.toRadians(rotation);
|
||||||
double locationX = image.getWidth() / 2;
|
double locationX = (double) image.getWidth() / 2;
|
||||||
double locationY = image.getHeight() / 2;
|
double locationY = (double) image.getHeight() / 2;
|
||||||
AffineTransform tx =
|
AffineTransform tx =
|
||||||
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
|
AffineTransform.getRotateInstance(rotationRequired, locationX, locationY);
|
||||||
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
|
AffineTransformOp op = new AffineTransformOp(tx, AffineTransformOp.TYPE_BICUBIC);
|
||||||
@ -127,8 +127,8 @@ public class FakeScanControllerWIP {
|
|||||||
|
|
||||||
for (int i = -radius; i <= radius; i++) {
|
for (int i = -radius; i <= radius; i++) {
|
||||||
for (int j = -radius; j <= radius; j++) {
|
for (int j = -radius; j <= radius; j++) {
|
||||||
double xDistance = i * i;
|
double xDistance = (double) i * i;
|
||||||
double yDistance = j * j;
|
double yDistance = (double) j * j;
|
||||||
double g = Math.exp(-(xDistance + yDistance) / (2 * sigma * sigma));
|
double g = Math.exp(-(xDistance + yDistance) / (2 * sigma * sigma));
|
||||||
data[(i + radius) * size + j + radius] = (float) g;
|
data[(i + radius) * size + j + radius] = (float) g;
|
||||||
sum += g;
|
sum += g;
|
||||||
@ -137,7 +137,7 @@ public class FakeScanControllerWIP {
|
|||||||
|
|
||||||
// Normalize the kernel
|
// Normalize the kernel
|
||||||
for (int i = 0; i < data.length; i++) {
|
for (int i = 0; i < data.length; i++) {
|
||||||
data[i] /= sum;
|
if (sum != 0) data[i] /= sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
Kernel kernel = new Kernel(size, size, data);
|
Kernel kernel = new Kernel(size, size, data);
|
||||||
@ -166,7 +166,7 @@ public class FakeScanControllerWIP {
|
|||||||
0,
|
0,
|
||||||
new Color(0, 0, 0, 1f),
|
new Color(0, 0, 0, 1f),
|
||||||
0,
|
0,
|
||||||
featherRadius * 2,
|
featherRadius * 2f,
|
||||||
new Color(0, 0, 0, 0f)));
|
new Color(0, 0, 0, 0f)));
|
||||||
g2.fillRect(0, 0, width, featherRadius);
|
g2.fillRect(0, 0, width, featherRadius);
|
||||||
|
|
||||||
@ -174,7 +174,7 @@ public class FakeScanControllerWIP {
|
|||||||
g2.setPaint(
|
g2.setPaint(
|
||||||
new GradientPaint(
|
new GradientPaint(
|
||||||
0,
|
0,
|
||||||
height - featherRadius * 2,
|
height - featherRadius * 2f,
|
||||||
new Color(0, 0, 0, 0f),
|
new Color(0, 0, 0, 0f),
|
||||||
0,
|
0,
|
||||||
height,
|
height,
|
||||||
@ -187,7 +187,7 @@ public class FakeScanControllerWIP {
|
|||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
new Color(0, 0, 0, 1f),
|
new Color(0, 0, 0, 1f),
|
||||||
featherRadius * 2,
|
featherRadius * 2f,
|
||||||
0,
|
0,
|
||||||
new Color(0, 0, 0, 0f)));
|
new Color(0, 0, 0, 0f)));
|
||||||
g2.fillRect(0, 0, featherRadius, height);
|
g2.fillRect(0, 0, featherRadius, height);
|
||||||
@ -195,7 +195,7 @@ public class FakeScanControllerWIP {
|
|||||||
// Right edge
|
// Right edge
|
||||||
g2.setPaint(
|
g2.setPaint(
|
||||||
new GradientPaint(
|
new GradientPaint(
|
||||||
width - featherRadius * 2,
|
width - featherRadius * 2f,
|
||||||
0,
|
0,
|
||||||
new Color(0, 0, 0, 0f),
|
new Color(0, 0, 0, 0f),
|
||||||
width,
|
width,
|
||||||
@ -244,7 +244,7 @@ public class FakeScanControllerWIP {
|
|||||||
int y2 = y1 + random.nextInt(20) - 10;
|
int y2 = y1 + random.nextInt(20) - 10;
|
||||||
Path2D.Double hair = new Path2D.Double();
|
Path2D.Double hair = new Path2D.Double();
|
||||||
hair.moveTo(x1, y1);
|
hair.moveTo(x1, y1);
|
||||||
hair.curveTo(x1, y1, (x1 + x2) / 2, (y1 + y2) / 2, x2, y2);
|
hair.curveTo(x1, y1, (double) (x1 + x2) / 2, (double) (y1 + y2) / 2, x2, y2);
|
||||||
g2d.draw(hair);
|
g2d.draw(hair);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
|
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class FlattenController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FlattenController.class);
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||||
|
@Operation(
|
||||||
|
summary = "Flatten PDF form fields or full page",
|
||||||
|
description =
|
||||||
|
"Flattening just PDF form fields or converting each page to images to make text unselectable. Input: PDF, Output: PDF. Type: SISO")
|
||||||
|
public ResponseEntity<byte[]> flatten(@ModelAttribute FlattenRequest request) throws Exception {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
|
||||||
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
PdfMetadata metadata = PdfUtils.extractMetadataFromPdf(document);
|
||||||
|
Boolean flattenOnlyForms = request.getFlattenOnlyForms();
|
||||||
|
|
||||||
|
if (Boolean.TRUE.equals(flattenOnlyForms)) {
|
||||||
|
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
|
||||||
|
if (acroForm != null) {
|
||||||
|
acroForm.flatten();
|
||||||
|
}
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document, Filenames.toSimpleFileName(file.getOriginalFilename()));
|
||||||
|
} else {
|
||||||
|
// flatten whole page aka convert each page to image and readd it (making text
|
||||||
|
// unselectable)
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
PDDocument newDocument = new PDDocument();
|
||||||
|
int numPages = document.getNumberOfPages();
|
||||||
|
for (int i = 0; i < numPages; i++) {
|
||||||
|
try {
|
||||||
|
BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB);
|
||||||
|
PDPage page = new PDPage();
|
||||||
|
page.setMediaBox(document.getPage(i).getMediaBox());
|
||||||
|
newDocument.addPage(page);
|
||||||
|
try (PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(newDocument, page)) {
|
||||||
|
PDImageXObject pdImage = JPEGFactory.createFromImage(newDocument, image);
|
||||||
|
float pageWidth = page.getMediaBox().getWidth();
|
||||||
|
float pageHeight = page.getMediaBox().getHeight();
|
||||||
|
|
||||||
|
contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("exception", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PdfUtils.setMetadataToPdf(newDocument, metadata);
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
newDocument, Filenames.toSimpleFileName(file.getOriginalFilename()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,8 @@ import org.apache.pdfbox.Loader;
|
|||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
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;
|
||||||
@ -30,6 +32,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class MetadataController {
|
public class MetadataController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(MetadataController.class);
|
||||||
|
|
||||||
private String checkUndefined(String entry) {
|
private String checkUndefined(String entry) {
|
||||||
// Check if the string is "undefined"
|
// Check if the string is "undefined"
|
||||||
if ("undefined".equals(entry)) {
|
if ("undefined".equals(entry)) {
|
||||||
@ -136,7 +140,7 @@ public class MetadataController {
|
|||||||
creationDateCal.setTime(
|
creationDateCal.setTime(
|
||||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
|
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate));
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
info.setCreationDate(creationDateCal);
|
info.setCreationDate(creationDateCal);
|
||||||
} else {
|
} else {
|
||||||
@ -148,7 +152,7 @@ public class MetadataController {
|
|||||||
modificationDateCal.setTime(
|
modificationDateCal.setTime(
|
||||||
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
|
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate));
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
info.setModificationDate(modificationDateCal);
|
info.setModificationDate(modificationDateCal);
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,7 +5,6 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -91,14 +90,12 @@ public class OCRController {
|
|||||||
}
|
}
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
|
|
||||||
// Prepare the output file path
|
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Prepare the output file path
|
|
||||||
Path sidecarTextPath = null;
|
Path sidecarTextPath = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
|
||||||
// Run OCR Command
|
// Run OCR Command
|
||||||
String languageOption = String.join("+", selectedLanguages);
|
String languageOption = String.join("+", selectedLanguages);
|
||||||
|
|
||||||
@ -151,7 +148,8 @@ public class OCRController {
|
|||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
if (result.getRc() != 0
|
if (result.getRc() != 0
|
||||||
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
||||||
&& result.getMessages().contains("OSError: [Errno 38] Function not implemented")) {
|
&& result.getMessages()
|
||||||
|
.contains("OSError: [Errno 38] Function not implemented")) {
|
||||||
command.add("--jobs");
|
command.add("--jobs");
|
||||||
command.add("1");
|
command.add("1");
|
||||||
result =
|
result =
|
||||||
@ -178,8 +176,6 @@ public class OCRController {
|
|||||||
}
|
}
|
||||||
// Read the OCR processed PDF file
|
// Read the OCR processed PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempInputFile);
|
|
||||||
|
|
||||||
// Return the OCR processed PDF as a response
|
// Return the OCR processed PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
@ -213,17 +209,26 @@ public class OCRController {
|
|||||||
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||||
|
|
||||||
// Clean up the temporary zip file
|
// Clean up the temporary zip file
|
||||||
Files.delete(tempZipFile);
|
Files.deleteIfExists(tempZipFile);
|
||||||
Files.delete(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
Files.delete(sidecarTextPath);
|
Files.deleteIfExists(sidecarTextPath);
|
||||||
|
|
||||||
// Return the zip file containing both the PDF and the text file
|
// Return the zip file containing both the PDF and the text file
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} else {
|
} else {
|
||||||
// Return the OCR processed PDF as a response
|
// Return the OCR processed PDF as a response
|
||||||
Files.delete(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
// Comment out as transferTo makes multipart handle cleanup
|
||||||
|
// Files.deleteIfExists(tempInputFile);
|
||||||
|
if (sidecarTextPath != null) {
|
||||||
|
Files.deleteIfExists(sidecarTextPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,10 +41,10 @@ public class RepairController {
|
|||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
|
||||||
|
|
||||||
// Prepare the output file path
|
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
byte[] pdfBytes = null;
|
||||||
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
try {
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("gs");
|
command.add("gs");
|
||||||
@ -58,11 +58,7 @@ public class RepairController {
|
|||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempInputFile);
|
|
||||||
Files.delete(tempOutputFile);
|
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
@ -70,5 +66,10 @@ public class RepairController {
|
|||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_repaired.pdf";
|
+ "_repaired.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary files
|
||||||
|
Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,10 +185,12 @@ public class StampController {
|
|||||||
try (InputStream is = classPathResource.getInputStream();
|
try (InputStream is = classPathResource.getInputStream();
|
||||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||||
IOUtils.copy(is, os);
|
IOUtils.copy(is, os);
|
||||||
}
|
|
||||||
|
|
||||||
font = PDType0Font.load(document, tempFile);
|
font = PDType0Font.load(document, tempFile);
|
||||||
tempFile.deleteOnExit();
|
} finally {
|
||||||
|
if (tempFile != null) {
|
||||||
|
Files.deleteIfExists(tempFile.toPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStream.setFont(font, fontSize);
|
contentStream.setFont(font, fontSize);
|
||||||
|
@ -19,6 +19,7 @@ import java.util.stream.Stream;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@ -28,6 +29,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
|
import stirling.software.SPDF.utils.FileMonitor;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PipelineDirectoryProcessor {
|
public class PipelineDirectoryProcessor {
|
||||||
@ -35,11 +37,18 @@ public class PipelineDirectoryProcessor {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
|
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
|
||||||
@Autowired private ObjectMapper objectMapper;
|
@Autowired private ObjectMapper objectMapper;
|
||||||
@Autowired private ApiDocService apiDocService;
|
@Autowired private ApiDocService apiDocService;
|
||||||
|
|
||||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
|
||||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
|
||||||
|
|
||||||
@Autowired PipelineProcessor processor;
|
@Autowired PipelineProcessor processor;
|
||||||
|
@Autowired FileMonitor fileMonitor;
|
||||||
|
|
||||||
|
final String watchedFoldersDir;
|
||||||
|
final String finishedFoldersDir;
|
||||||
|
|
||||||
|
public PipelineDirectoryProcessor(
|
||||||
|
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
|
||||||
|
@Qualifier("finishedFoldersDir") String finishedFoldersDir) {
|
||||||
|
this.watchedFoldersDir = watchedFoldersDir;
|
||||||
|
this.finishedFoldersDir = finishedFoldersDir;
|
||||||
|
}
|
||||||
|
|
||||||
@Scheduled(fixedRate = 60000)
|
@Scheduled(fixedRate = 60000)
|
||||||
public void scanFolders() {
|
public void scanFolders() {
|
||||||
@ -130,7 +139,11 @@ public class PipelineDirectoryProcessor {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
try (Stream<Path> paths = Files.list(dir)) {
|
try (Stream<Path> paths = Files.list(dir)) {
|
||||||
if ("automated".equals(operation.getParameters().get("fileInput"))) {
|
if ("automated".equals(operation.getParameters().get("fileInput"))) {
|
||||||
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
|
return paths.filter(
|
||||||
|
path ->
|
||||||
|
!Files.isDirectory(path)
|
||||||
|
&& !path.equals(jsonFile)
|
||||||
|
&& fileMonitor.isFileReadyForProcessing(path))
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.toArray(File[]::new);
|
.toArray(File[]::new);
|
||||||
} else {
|
} else {
|
||||||
|
@ -105,8 +105,15 @@ 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()) {
|
||||||
|
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());
|
body.add(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
@ -167,8 +174,15 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
|
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());
|
body.add(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ public class CertSignController {
|
|||||||
doc.addSignature(signature, instance);
|
doc.addSignature(signature, instance);
|
||||||
doc.saveIncremental(output);
|
doc.saveIncremental(output);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ import org.apache.xmpbox.XMPMetadata;
|
|||||||
import org.apache.xmpbox.xml.DomXmpParser;
|
import org.apache.xmpbox.xml.DomXmpParser;
|
||||||
import org.apache.xmpbox.xml.XmpParsingException;
|
import org.apache.xmpbox.xml.XmpParsingException;
|
||||||
import org.apache.xmpbox.xml.XmpSerializer;
|
import org.apache.xmpbox.xml.XmpSerializer;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
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;
|
||||||
@ -79,6 +81,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Security", description = "Security APIs")
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
public class GetInfoOnPDF {
|
public class GetInfoOnPDF {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GetInfoOnPDF.class);
|
||||||
|
|
||||||
static ObjectMapper objectMapper = new ObjectMapper();
|
static ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
|
||||||
@ -220,7 +224,7 @@ public class GetInfoOnPDF {
|
|||||||
javascriptArray.add(jsNode);
|
javascriptArray.add(jsNode);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -253,7 +257,7 @@ public class GetInfoOnPDF {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A");
|
||||||
@ -305,7 +309,7 @@ public class GetInfoOnPDF {
|
|||||||
new XmpSerializer().serialize(xmpMeta, os, true);
|
new XmpSerializer().serialize(xmpMeta, os, true);
|
||||||
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
|
||||||
} catch (XmpParsingException | IOException e) {
|
} catch (XmpParsingException | IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,7 +597,7 @@ public class GetInfoOnPDF {
|
|||||||
MediaType.APPLICATION_JSON);
|
MediaType.APPLICATION_JSON);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -691,7 +695,7 @@ public class GetInfoOnPDF {
|
|||||||
Exception
|
Exception
|
||||||
e) { // Catching general exception for brevity, ideally you'd catch specific
|
e) { // Catching general exception for brevity, ideally you'd catch specific
|
||||||
// exceptions.
|
// exceptions.
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
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.LoggerFactory;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/security")
|
||||||
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
|
public class RemoveCertSignController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RemoveCertSignController.class);
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-cert-sign")
|
||||||
|
@Operation(
|
||||||
|
summary = "Remove digital signature from PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint accepts a PDF file and returns the PDF file without the digital signature. Input: PDF, Output: PDF")
|
||||||
|
public ResponseEntity<byte[]> removeCertSignPDF(@ModelAttribute PDFFile request)
|
||||||
|
throws Exception {
|
||||||
|
MultipartFile pdf = request.getFileInput();
|
||||||
|
|
||||||
|
// Convert MultipartFile to byte[]
|
||||||
|
byte[] pdfBytes = pdf.getBytes();
|
||||||
|
|
||||||
|
// Create a ByteArrayOutputStream to hold the resulting PDF
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// Load the PDF document
|
||||||
|
PDDocument document = Loader.loadPDF(pdfBytes);
|
||||||
|
|
||||||
|
// Get the document catalog
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
|
||||||
|
// Get the AcroForm
|
||||||
|
PDAcroForm acroForm = catalog.getAcroForm();
|
||||||
|
if (acroForm != null) {
|
||||||
|
// Remove signature fields safely
|
||||||
|
List<PDField> fieldsToRemove =
|
||||||
|
acroForm.getFields().stream()
|
||||||
|
.filter(field -> field instanceof PDSignatureField)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
|
acroForm.flatten(fieldsToRemove, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the modified document to the ByteArrayOutputStream
|
||||||
|
document.save(baos);
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
// Return the modified PDF as a response
|
||||||
|
return WebResponseUtils.boasToWebResponse(
|
||||||
|
baos,
|
||||||
|
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_unsigned.pdf");
|
||||||
|
}
|
||||||
|
}
|
@ -150,10 +150,10 @@ public class WatermarkController {
|
|||||||
try (InputStream is = classPathResource.getInputStream();
|
try (InputStream is = classPathResource.getInputStream();
|
||||||
FileOutputStream os = new FileOutputStream(tempFile)) {
|
FileOutputStream os = new FileOutputStream(tempFile)) {
|
||||||
IOUtils.copy(is, os);
|
IOUtils.copy(is, os);
|
||||||
}
|
|
||||||
|
|
||||||
font = PDType0Font.load(document, tempFile);
|
font = PDType0Font.load(document, tempFile);
|
||||||
tempFile.deleteOnExit();
|
} finally {
|
||||||
|
if (tempFile != null) Files.deleteIfExists(tempFile.toPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
contentStream.setFont(font, fontSize);
|
contentStream.setFont(font, fontSize);
|
||||||
|
@ -117,7 +117,6 @@ public class PDFTableStripper extends PDFTextStripper {
|
|||||||
/**
|
/**
|
||||||
* Instantiate a new PDFTableStripper object.
|
* Instantiate a new PDFTableStripper object.
|
||||||
*
|
*
|
||||||
* @param document
|
|
||||||
* @throws IOException If there is an error loading the properties.
|
* @throws IOException If there is an error loading the properties.
|
||||||
*/
|
*/
|
||||||
public PDFTableStripper() throws IOException {
|
public PDFTableStripper() throws IOException {
|
||||||
|