Compare commits
239 Commits
2330813aa9
...
7c1ad19e26
Author | SHA1 | Date | |
---|---|---|---|
|
7c1ad19e26 | ||
|
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 | ||
|
ce3e98e240 | ||
|
36192ba560 | ||
|
0e262dc2bd | ||
|
c1fea7c92f | ||
|
7ba0067688 | ||
|
caa5525ddd |
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
|
||||||
|
5
.gitignore
vendored
@ -124,4 +124,7 @@ 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
|
||||||
|
|
||||||
|
25
Dockerfile
@ -1,5 +1,5 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:20240329
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@ -10,35 +10,33 @@ 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 \
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
PUID=1000 \
|
PUID=1000 \
|
||||||
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 \
|
||||||
openjdk21-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
|
||||||
|
|
||||||
@ -8,7 +8,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
HOME=/home/stirlingpdfuser \
|
HOME=/home/stirlingpdfuser \
|
||||||
VERSION_TAG=$VERSION_TAG \
|
VERSION_TAG=$VERSION_TAG \
|
||||||
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \
|
||||||
PUID=1000 \
|
PUID=1000 \
|
||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022
|
UMASK=022
|
||||||
|
|
||||||
@ -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 \
|
||||||
|
su-exec \
|
||||||
openjdk21-jre && \
|
openjdk21-jre && \
|
||||||
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 && \
|
|
||||||
# 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,46 +1,47 @@
|
|||||||
| 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 | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
| extract-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
|
||||||
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
| remove-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
| scale-pages | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
| split-pdfs | ✔️ | | | | | | | | | ✔️ | |
|
||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
| add-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
| add-watermark | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
| cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
| change-permissions | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
| remove-password | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
|
||||||
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
| add-image | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
| auto-rename | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ |
|
| change-metadata | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| compare | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
| extract-images | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
| flatten | | | | ✔️ | | | | | | | ✔️ |
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ |
|
| show-javascript | | | | ✔️ | | | | | | | ✔️ |
|
||||||
|
| sign | | | | ✔️ | | | | | | | ✔️ |
|
80
README.md
@ -5,7 +5,7 @@
|
|||||||
[![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)
|
||||||
@ -159,38 +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) | ![41%](https://geps.dev/progress/41) |
|
| Arabic (العربية) (ar_AR) | ![40%](https://geps.dev/progress/40) |
|
||||||
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
|
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
|
||||||
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
|
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Spanish (Español) (es_ES) | ![97%](https://geps.dev/progress/97) |
|
| Spanish (Español) (es_ES) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) |
|
| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
|
| Traditional Chinese (繁體中文) (zh_TW) | ![94%](https://geps.dev/progress/94) |
|
||||||
| Catalan (Català) (ca_CA) | ![50%](https://geps.dev/progress/50) |
|
| 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) | ![41%](https://geps.dev/progress/41) |
|
| Swedish (Svenska) (sv_SE) | ![40%](https://geps.dev/progress/40) |
|
||||||
| Polish (Polski) (pl_PL) | ![43%](https://geps.dev/progress/43) |
|
| Polish (Polski) (pl_PL) | ![42%](https://geps.dev/progress/42) |
|
||||||
| Romanian (Română) (ro_RO) | ![40%](https://geps.dev/progress/40) |
|
| Romanian (Română) (ro_RO) | ![39%](https://geps.dev/progress/39) |
|
||||||
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
|
| Korean (한국어) (ko_KR) | ![87%](https://geps.dev/progress/87) |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) | ![62%](https://geps.dev/progress/62) |
|
| Portuguese Brazilian (Português) (pt_BR) | ![61%](https://geps.dev/progress/61) |
|
||||||
| Russian (Русский) (ru_RU) | ![89%](https://geps.dev/progress/89) |
|
| Russian (Русский) (ru_RU) | ![86%](https://geps.dev/progress/86) |
|
||||||
| Basque (Euskara) (eu_ES) | ![65%](https://geps.dev/progress/65) |
|
| Basque (Euskara) (eu_ES) | ![63%](https://geps.dev/progress/63) |
|
||||||
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) |
|
| Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) |
|
||||||
| Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) |
|
| Dutch (Nederlands) (nl_NL) | ![84%](https://geps.dev/progress/84) |
|
||||||
| Greek (Ελληνικά) (el_GR) | ![87%](https://geps.dev/progress/87) |
|
| 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) | ![80%](https://geps.dev/progress/80) |
|
| Indonesia (Bahasa Indonesia) (id_ID) | ![78%](https://geps.dev/progress/78) |
|
||||||
| Hindi (हिंदी) (hi_IN) | ![81%](https://geps.dev/progress/81) |
|
| Hindi (हिंदी) (hi_IN) | ![79%](https://geps.dev/progress/79) |
|
||||||
| Hungarian (Magyar) (hu_HU) | ![79%](https://geps.dev/progress/79) |
|
| Hungarian (Magyar) (hu_HU) | ![77%](https://geps.dev/progress/77) |
|
||||||
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
|
| Bulgarian (Български) (bg_BG) | ![97%](https://geps.dev/progress/97) |
|
||||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![82%](https://geps.dev/progress/82) |
|
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![80%](https://geps.dev/progress/80) |
|
||||||
| Ukrainian (Українська) (uk_UA) | ![88%](https://geps.dev/progress/88) |
|
| Ukrainian (Українська) (uk_UA) | ![86%](https://geps.dev/progress/86) |
|
||||||
| Slovakian (Slovensky) (sk_SK) | ![96%](https://geps.dev/progress/96) |
|
| 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.)
|
||||||
|
|
||||||
@ -212,10 +215,10 @@ 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
|
||||||
|
|
||||||
@ -224,9 +227,9 @@ security:
|
|||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries
|
loginAttemptCount: 5 # lock user account after 5 tries
|
||||||
loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
# initialLogin:
|
# initialLogin:
|
||||||
# username: "admin" # Initial username for the first login (these are defaulted)
|
# username: "admin" # Initial username for the first login
|
||||||
# password: "stirling" # Initial password for the first login
|
# password: "stirling" # Initial password for the first login
|
||||||
# oauth2:
|
# oauth2:
|
||||||
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
@ -237,6 +240,23 @@ security:
|
|||||||
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
|
# 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
|
# 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'
|
# 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)
|
||||||
|
@ -1,52 +1,53 @@
|
|||||||
| Technology | Ultra-Lite | Full |
|
| Technology | Ultra-Lite | Full |
|
||||||
|----------------|:----------:|:----:|
|
| ---------- | :--------: | :---: |
|
||||||
| Java | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ |
|
||||||
| Libre | | ✔️ |
|
| Libre | | ✔️ |
|
||||||
| Python | | ✔️ |
|
| Python | | ✔️ |
|
||||||
| 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 | | ✔️ |
|
||||||
|
21
build.gradle
@ -12,7 +12,7 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
group = 'stirling.software'
|
group = 'stirling.software'
|
||||||
version = '0.24.4'
|
version = '0.25.3'
|
||||||
|
|
||||||
//17 is lowest but we support and recommend 21
|
//17 is lowest but we support and recommend 21
|
||||||
sourceCompatibility = '17'
|
sourceCompatibility = '17'
|
||||||
@ -21,7 +21,6 @@ repositories {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
renderers = [new JsonReportRenderer()]
|
renderers = [new JsonReportRenderer()]
|
||||||
}
|
}
|
||||||
@ -94,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') {
|
||||||
@ -166,14 +171,14 @@ 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 {
|
||||||
options.compilerArgs << '-parameters'
|
options.compilerArgs << '-parameters'
|
||||||
}
|
}
|
||||||
|
|
||||||
task writeVersion {
|
task writeVersion {
|
||||||
def propsFile = file('src/main/resources/version.properties')
|
def propsFile = file('src/main/resources/version.properties')
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
props.setProperty('version', version)
|
props.setProperty('version', version)
|
||||||
@ -190,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 {
|
||||||
@ -205,6 +208,6 @@ tasks.named('test') {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
task printVersion {
|
task printVersion {
|
||||||
println project.version
|
println project.version
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.24.4
|
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,14 +11,16 @@ 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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Setting permissions and ownership for necessary directories..."
|
echo "Setting permissions and ownership for necessary directories..."
|
||||||
|
@ -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',
|
||||||
|
@ -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");
|
try (Socket socket = new Socket()) {
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,7 @@ import java.net.URISyntaxException;
|
|||||||
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.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
@ -49,45 +44,47 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Path templatePath =
|
// Path templatePath =
|
||||||
Paths.get(
|
// Paths.get(
|
||||||
getClass()
|
// getClass()
|
||||||
.getClassLoader()
|
// .getClassLoader()
|
||||||
.getResource("settings.yml.template")
|
// .getResource("settings.yml.template")
|
||||||
.toURI());
|
// .toURI());
|
||||||
Path userPath = Paths.get("configs", "settings.yml");
|
// Path userPath = Paths.get("configs", "settings.yml");
|
||||||
|
//
|
||||||
List<String> templateLines = Files.readAllLines(templatePath);
|
// List<String> templateLines = Files.readAllLines(templatePath);
|
||||||
List<String> userLines =
|
// List<String> userLines =
|
||||||
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
|
// Files.exists(userPath) ? Files.readAllLines(userPath) : new
|
||||||
|
// ArrayList<>();
|
||||||
List<String> resultLines = new ArrayList<>();
|
//
|
||||||
|
// List<String> resultLines = new ArrayList<>();
|
||||||
for (String templateLine : templateLines) {
|
// int position = 0;
|
||||||
// Check if the line is a comment
|
// for (String templateLine : templateLines) {
|
||||||
if (templateLine.trim().startsWith("#")) {
|
// // Check if the line is a comment
|
||||||
String entry = templateLine.trim().substring(1).trim();
|
// if (templateLine.trim().startsWith("#")) {
|
||||||
if (!entry.isEmpty()) {
|
// String entry = templateLine.trim().substring(1).trim();
|
||||||
// Check if this comment has been uncommented in userLines
|
// if (!entry.isEmpty()) {
|
||||||
String key = entry.split(":")[0].trim();
|
// // Check if this comment has been uncommented in userLines
|
||||||
addLine(resultLines, userLines, templateLine, key);
|
// String key = entry.split(":")[0].trim();
|
||||||
} else {
|
// addLine(resultLines, userLines, templateLine, key, position);
|
||||||
resultLines.add(templateLine);
|
// } else {
|
||||||
}
|
// resultLines.add(templateLine);
|
||||||
}
|
// }
|
||||||
// Check if the line is a key-value pair
|
// }
|
||||||
else if (templateLine.contains(":")) {
|
// // Check if the line is a key-value pair
|
||||||
String key = templateLine.split(":")[0].trim();
|
// else if (templateLine.contains(":")) {
|
||||||
addLine(resultLines, userLines, templateLine, key);
|
// String key = templateLine.split(":")[0].trim();
|
||||||
}
|
// addLine(resultLines, userLines, templateLine, key, position);
|
||||||
// Handle empty lines
|
// }
|
||||||
else if (templateLine.trim().length() == 0) {
|
// // Handle empty lines
|
||||||
resultLines.add("");
|
// else if (templateLine.trim().length() == 0) {
|
||||||
}
|
// resultLines.add("");
|
||||||
}
|
// }
|
||||||
|
// position++;
|
||||||
// Write the result to the user settings file
|
// }
|
||||||
Files.write(userPath, resultLines);
|
//
|
||||||
|
// // Write the result to the user settings file
|
||||||
|
// Files.write(userPath, resultLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
||||||
@ -95,15 +92,19 @@ public class ConfigInitializer
|
|||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
||||||
|
private static void addLine(
|
||||||
//TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
|
List<String> resultLines,
|
||||||
private static void addLine(List<String> resultLines, List<String> userLines, String templateLine, String key) {
|
List<String> userLines,
|
||||||
|
String templateLine,
|
||||||
|
String key,
|
||||||
|
int position) {
|
||||||
boolean added = false;
|
boolean added = false;
|
||||||
int templateIndentationLevel = getIndentationLevel(templateLine);
|
int templateIndentationLevel = getIndentationLevel(templateLine);
|
||||||
|
int pos = 0;
|
||||||
for (String settingsLine : userLines) {
|
for (String settingsLine : userLines) {
|
||||||
if (settingsLine.trim().startsWith(key + ":")) {
|
if (settingsLine.trim().startsWith(key + ":") && position == pos) {
|
||||||
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
int settingsIndentationLevel = getIndentationLevel(settingsLine);
|
||||||
// Check if it is correct settingsLine and has the same parent as templateLine
|
// Check if it is correct settingsLine and has the same parent as templateLine
|
||||||
if (settingsIndentationLevel == templateIndentationLevel) {
|
if (settingsIndentationLevel == templateIndentationLevel) {
|
||||||
@ -112,17 +113,18 @@ public class ConfigInitializer
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pos++;
|
||||||
}
|
}
|
||||||
if (!added) {
|
if (!added) {
|
||||||
resultLines.add(templateLine);
|
resultLines.add(templateLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getIndentationLevel(String line) {
|
private static int getIndentationLevel(String line) {
|
||||||
int indentationLevel = 0;
|
int indentationLevel = 0;
|
||||||
String trimmedLine = line.trim();
|
String trimmedLine = line.trim();
|
||||||
if (trimmedLine.startsWith("#")) {
|
if (trimmedLine.startsWith("#")) {
|
||||||
line = trimmedLine.substring(1);
|
line = trimmedLine.substring(1);
|
||||||
}
|
}
|
||||||
for (char c : line.toCharArray()) {
|
for (char c : line.toCharArray()) {
|
||||||
if (c == ' ') {
|
if (c == ' ') {
|
||||||
|
@ -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) {
|
||||||
|
@ -42,36 +42,39 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
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)
|
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
||||||
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
response.sendRedirect("/login?error=oauth2AuthenticationError");
|
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = request.getParameter("username");
|
String username = request.getParameter("username");
|
||||||
if (username != null && !isDemoUser(username)) {
|
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"Remaining attempts for user {}: {}",
|
"Remaining attempts for user {}: {}",
|
||||||
username,
|
optUser.get().getUsername(),
|
||||||
loginAttemptService.getRemainingAttempts(username));
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
loginAttemptService.loginFailed(username);
|
loginAttemptService.loginFailed(username);
|
||||||
if (loginAttemptService.isBlocked(username)
|
if (loginAttemptService.isBlocked(username)
|
||||||
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
response.sendRedirect("/login?error=locked");
|
response.sendRedirect(contextPath + "/login?error=locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
||||||
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
||||||
response.sendRedirect("/login?error=badcredentials");
|
response.sendRedirect(contextPath + "/login?error=badcredentials");
|
||||||
return;
|
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()));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,6 @@ public class LoginAttemptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loginSucceeded(String key) {
|
public void loginSucceeded(String key) {
|
||||||
logger.info(key + " " + attemptsCache.mappingCount());
|
|
||||||
if (key == null || key.trim().isEmpty()) {
|
if (key == null || key.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
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;
|
||||||
@ -36,7 +38,11 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationS
|
|||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.GithubProvider;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.GoogleProvider;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.KeycloakProvider;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@ -47,6 +53,8 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
@ -140,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");
|
||||||
@ -150,7 +159,8 @@ public class SecurityConfiguration {
|
|||||||
.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(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
@ -181,9 +191,10 @@ public class SecurityConfiguration {
|
|||||||
.logout(
|
.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutSuccessHandler(
|
logout.logoutSuccessHandler(
|
||||||
new CustomOAuth2LogoutSuccessHandler(
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
this.applicationProperties,
|
this.applicationProperties,
|
||||||
sessionRegistry())));
|
sessionRegistry()))
|
||||||
|
.invalidateHttpSession(true));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
@ -200,19 +211,127 @@ public class SecurityConfiguration {
|
|||||||
havingValue = "true",
|
havingValue = "true",
|
||||||
matchIfMissing = false)
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientRegistration oidcClientRegistration() {
|
private Optional<ClientRegistration> googleClientRegistration() {
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
return ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
.registrationId("oidc")
|
return Optional.empty();
|
||||||
.clientId(oauth.getClientId())
|
}
|
||||||
.clientSecret(oauth.getClientSecret())
|
Client client = oauth.getClient();
|
||||||
.scope(oauth.getScopes())
|
if (client == null) {
|
||||||
.userNameAttributeName(oauth.getUseAsUsername())
|
return Optional.empty();
|
||||||
.clientName("OIDC")
|
}
|
||||||
.build();
|
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")
|
||||||
|
.clientId(oauth.getClientId())
|
||||||
|
.clientSecret(oauth.getClientSecret())
|
||||||
|
.scope(oauth.getScopes())
|
||||||
|
.userNameAttributeName(oauth.getUseAsUsername())
|
||||||
|
.clientName("OIDC")
|
||||||
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -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",
|
||||||
|
@ -41,6 +41,7 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
} else if (exception instanceof LockedException) {
|
} else if (exception instanceof LockedException) {
|
||||||
logger.error("Account locked: ", exception);
|
logger.error("Account locked: ", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
logger.error("Unhandled authentication exception", exception);
|
logger.error("Unhandled authentication exception", exception);
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
@ -6,6 +6,7 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@ -14,6 +15,8 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.Provider;
|
||||||
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
@ -33,54 +36,87 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
String param = "logout=true";
|
String param = "logout=true";
|
||||||
|
String registrationId = null;
|
||||||
|
String issuer = null;
|
||||||
|
String clientId = null;
|
||||||
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
String provider = oauth.getProvider() != null ? oauth.getProvider() : "";
|
|
||||||
|
|
||||||
|
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) {
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||||
} else if (request.getParameter("error") != null) {
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
param = "error=" + request.getParameter("error");
|
param = "error=" + sanitizeInput(errorMessage);
|
||||||
} else if (request.getParameter("erroroauth") != null) {
|
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
||||||
param = "erroroauth=" + request.getParameter("erroroauth");
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||||
|
|
||||||
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.removeSessionInformation(sessionId);
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
session.invalidate();
|
session.invalidate();
|
||||||
logger.debug("Session invalidated: " + sessionId);
|
logger.info("Session invalidated: " + sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (provider) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
|
// Add Keycloak specific logout URL if needed
|
||||||
String logoutUrl =
|
String logoutUrl =
|
||||||
oauth.getIssuer()
|
issuer
|
||||||
+ "/protocol/openid-connect/logout"
|
+ "/protocol/openid-connect/logout"
|
||||||
+ "?client_id="
|
+ "?client_id="
|
||||||
+ oauth.getClientId()
|
+ clientId
|
||||||
+ "&post_logout_redirect_uri="
|
+ "&post_logout_redirect_uri="
|
||||||
+ response.encodeRedirectURL(
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
request.getScheme()
|
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
+ "://"
|
|
||||||
+ request.getHeader("host")
|
|
||||||
+ "/login?"
|
|
||||||
+ param);
|
|
||||||
logger.debug("Redirecting to Keycloak logout URL: " + logoutUrl);
|
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
break;
|
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":
|
case "google":
|
||||||
// Add Google specific logout URL if needed
|
// 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:
|
default:
|
||||||
String redirectUrl = request.getContextPath() + "/login?" + param;
|
String redirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
logger.debug("Redirecting to default logout URL: " + redirectUrl);
|
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
||||||
response.sendRedirect(redirectUrl);
|
response.sendRedirect(redirectUrl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sanitizeInput(String input) {
|
||||||
|
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
|||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
@ -41,11 +43,27 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
String usernameAttribute =
|
OAUTH2 oauth2 = applicationProperties.getSecurity().getOAUTH2();
|
||||||
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
String usernameAttribute = oauth2.getUseAsUsername();
|
||||||
|
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
||||||
|
Client client = oauth2.getClient();
|
||||||
|
if (client != null && client.getKeycloak() != null) {
|
||||||
|
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
||||||
|
} else {
|
||||||
|
usernameAttribute = "email";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OidcUser user = delegate.loadUser(userRequest);
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
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);
|
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||||
if (duser.isPresent()) {
|
if (duser.isPresent()) {
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
@ -56,13 +74,14 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
throw new IllegalArgumentException("Password must not be null");
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a new OidcUser with adjusted attributes
|
// Return a new OidcUser with adjusted attributes
|
||||||
return new DefaultOidcUser(
|
return new DefaultOidcUser(
|
||||||
user.getAuthorities(),
|
user.getAuthorities(),
|
||||||
userRequest.getIdToken(),
|
userRequest.getIdToken(),
|
||||||
user.getUserInfo(),
|
user.getUserInfo(),
|
||||||
usernameAttribute);
|
usernameAttribute);
|
||||||
} catch (java.lang.IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
logger.error("Error loading OIDC user: {}", e.getMessage());
|
logger.error("Error loading OIDC user: {}", e.getMessage());
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -1,57 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.oauth2;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
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.model.ApplicationProperties;
|
|
||||||
|
|
||||||
public class CustomOAuthUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CustomOAuthUserService.class);
|
|
||||||
|
|
||||||
private final OidcUserService delegate = new OidcUserService();
|
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
public CustomOAuthUserService(ApplicationProperties applicationProperties) {
|
|
||||||
this.applicationProperties = applicationProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
|
||||||
String usernameAttribute =
|
|
||||||
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
|
|
||||||
try {
|
|
||||||
|
|
||||||
OidcUser user = delegate.loadUser(userRequest);
|
|
||||||
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
|
|
||||||
|
|
||||||
// Ensure the preferred username attribute is present
|
|
||||||
if (!attributes.containsKey(usernameAttribute)) {
|
|
||||||
attributes.put(usernameAttribute, attributes.getOrDefault("email", ""));
|
|
||||||
usernameAttribute = "email";
|
|
||||||
logger.info("Adjusted username attribute to use email");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a new OidcUser with adjusted attributes
|
|
||||||
return new DefaultOidcUser(
|
|
||||||
user.getAuthorities(),
|
|
||||||
userRequest.getIdToken(),
|
|
||||||
user.getUserInfo(),
|
|
||||||
usernameAttribute);
|
|
||||||
} catch (java.lang.IllegalArgumentException e) {
|
|
||||||
throw new OAuth2AuthenticationException(
|
|
||||||
new OAuth2Error(e.getMessage()), e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
@ -66,46 +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 {
|
try {
|
||||||
userService.changeUsername(user, newUsername);
|
userService.changeUsername(user, newUsername);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return new RedirectView("/account?messageType=invalidUsername");
|
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')")
|
||||||
@ -118,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);
|
||||||
@ -138,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')")
|
||||||
@ -151,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);
|
||||||
@ -171,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')")
|
||||||
@ -204,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);
|
||||||
@ -212,26 +212,27 @@ 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')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@ -244,33 +245,34 @@ public class UserController {
|
|||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (!userOpt.isPresent()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound");
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound");
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
// Get the currently authenticated username
|
// Get the currently authenticated username
|
||||||
String currentUsername = authentication.getName();
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
// Check if the provided username matches the current session's username
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=downgradeCurrentUser");
|
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", 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);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
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')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@ -279,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
|
||||||
@ -287,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;
|
||||||
|
@ -56,7 +56,7 @@ public class ConvertImgPDFController {
|
|||||||
String filename =
|
String filename =
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "");
|
||||||
|
|
||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
@ -65,10 +65,9 @@ public class ConvertImgPDFController {
|
|||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
filename);
|
filename);
|
||||||
|
|
||||||
|
|
||||||
if(result == null || result.length == 0) {
|
if (result == null || result.length == 0) {
|
||||||
logger.error("resultant bytes for {} is null, error converting ", filename);
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
String docName = filename + "." + imageFormat;
|
String docName = filename + "." + 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,34 +40,35 @@ 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");
|
||||||
|
|
||||||
// Run the LibreOffice command
|
try {
|
||||||
List<String> command =
|
// Run the LibreOffice command
|
||||||
new ArrayList<>(
|
List<String> command =
|
||||||
Arrays.asList(
|
new ArrayList<>(
|
||||||
"unoconv",
|
Arrays.asList(
|
||||||
"-vvv",
|
"unoconv",
|
||||||
"-f",
|
"-vvv",
|
||||||
"pdf",
|
"-f",
|
||||||
"-o",
|
"pdf",
|
||||||
tempOutputFile.toString(),
|
"-o",
|
||||||
tempInputFile.toString()));
|
tempOutputFile.toString(),
|
||||||
ProcessExecutorResult returnCode =
|
tempInputFile.toString()));
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
ProcessExecutorResult returnCode =
|
||||||
.runCommandWithOutputHandling(command);
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the converted PDF file
|
// Read the converted PDF file
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
return pdfBytes;
|
||||||
// Clean up the temporary files
|
} finally {
|
||||||
Files.delete(tempInputFile);
|
// Clean up the temporary files
|
||||||
Files.delete(tempOutputFile);
|
if (tempInputFile != null) Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
return pdfBytes;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
// Return the optimized PDF as a response
|
||||||
|
String outputFilename =
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_Optimized.pdf";
|
||||||
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.delete(tempInputFile);
|
// deleted by multipart file handler deu to transferTo?
|
||||||
Files.delete(tempOutputFile);
|
// Files.deleteIfExists(tempInputFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
|
||||||
String outputFilename =
|
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_Optimized.pdf";
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,8 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
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;
|
||||||
@ -33,6 +35,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class FlattenController {
|
public class FlattenController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FlattenController.class);
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
@PostMapping(consumes = "multipart/form-data", value = "/flatten")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Flatten PDF form fields or full page",
|
summary = "Flatten PDF form fields or full page",
|
||||||
@ -73,7 +77,7 @@ public class FlattenController {
|
|||||||
contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
|
contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PdfUtils.setMetadataToPdf(newDocument, metadata);
|
PdfUtils.setMetadataToPdf(newDocument, metadata);
|
||||||
|
@ -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,139 +90,145 @@ 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;
|
||||||
|
|
||||||
// Run OCR Command
|
try {
|
||||||
String languageOption = String.join("+", selectedLanguages);
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
|
||||||
List<String> command =
|
// Run OCR Command
|
||||||
new ArrayList<>(
|
String languageOption = String.join("+", selectedLanguages);
|
||||||
Arrays.asList(
|
|
||||||
"ocrmypdf",
|
|
||||||
"--verbose",
|
|
||||||
"2",
|
|
||||||
"--output-type",
|
|
||||||
"pdf",
|
|
||||||
"--pdf-renderer",
|
|
||||||
ocrRenderType));
|
|
||||||
|
|
||||||
if (sidecar != null && sidecar) {
|
List<String> command =
|
||||||
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
new ArrayList<>(
|
||||||
command.add("--sidecar");
|
Arrays.asList(
|
||||||
command.add(sidecarTextPath.toString());
|
"ocrmypdf",
|
||||||
}
|
"--verbose",
|
||||||
|
"2",
|
||||||
if (deskew != null && deskew) {
|
"--output-type",
|
||||||
command.add("--deskew");
|
"pdf",
|
||||||
}
|
"--pdf-renderer",
|
||||||
if (clean != null && clean) {
|
ocrRenderType));
|
||||||
command.add("--clean");
|
|
||||||
}
|
|
||||||
if (cleanFinal != null && cleanFinal) {
|
|
||||||
command.add("--clean-final");
|
|
||||||
}
|
|
||||||
if (ocrType != null && !"".equals(ocrType)) {
|
|
||||||
if ("skip-text".equals(ocrType)) {
|
|
||||||
command.add("--skip-text");
|
|
||||||
} else if ("force-ocr".equals(ocrType)) {
|
|
||||||
command.add("--force-ocr");
|
|
||||||
} else if ("Normal".equals(ocrType)) {
|
|
||||||
|
|
||||||
|
if (sidecar != null && sidecar) {
|
||||||
|
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
||||||
|
command.add("--sidecar");
|
||||||
|
command.add(sidecarTextPath.toString());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
command.addAll(
|
if (deskew != null && deskew) {
|
||||||
Arrays.asList(
|
command.add("--deskew");
|
||||||
"--language",
|
}
|
||||||
languageOption,
|
if (clean != null && clean) {
|
||||||
tempInputFile.toString(),
|
command.add("--clean");
|
||||||
tempOutputFile.toString()));
|
}
|
||||||
|
if (cleanFinal != null && cleanFinal) {
|
||||||
|
command.add("--clean-final");
|
||||||
|
}
|
||||||
|
if (ocrType != null && !"".equals(ocrType)) {
|
||||||
|
if ("skip-text".equals(ocrType)) {
|
||||||
|
command.add("--skip-text");
|
||||||
|
} else if ("force-ocr".equals(ocrType)) {
|
||||||
|
command.add("--force-ocr");
|
||||||
|
} else if ("Normal".equals(ocrType)) {
|
||||||
|
|
||||||
// Run CLI command
|
}
|
||||||
ProcessExecutorResult result =
|
}
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
|
||||||
.runCommandWithOutputHandling(command);
|
command.addAll(
|
||||||
if (result.getRc() != 0
|
Arrays.asList(
|
||||||
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
"--language",
|
||||||
&& result.getMessages().contains("OSError: [Errno 38] Function not implemented")) {
|
languageOption,
|
||||||
command.add("--jobs");
|
tempInputFile.toString(),
|
||||||
command.add("1");
|
tempOutputFile.toString()));
|
||||||
result =
|
|
||||||
|
// Run CLI command
|
||||||
|
ProcessExecutorResult result =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
}
|
if (result.getRc() != 0
|
||||||
|
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
||||||
// Remove images from the OCR processed PDF if the flag is set to true
|
&& result.getMessages()
|
||||||
if (removeImagesAfter != null && removeImagesAfter) {
|
.contains("OSError: [Errno 38] Function not implemented")) {
|
||||||
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
|
command.add("--jobs");
|
||||||
|
command.add("1");
|
||||||
List<String> gsCommand =
|
result =
|
||||||
Arrays.asList(
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
"gs",
|
.runCommandWithOutputHandling(command);
|
||||||
"-sDEVICE=pdfwrite",
|
|
||||||
"-dFILTERIMAGE",
|
|
||||||
"-o",
|
|
||||||
tempPdfWithoutImages.toString(),
|
|
||||||
tempOutputFile.toString());
|
|
||||||
|
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
|
||||||
.runCommandWithOutputHandling(gsCommand);
|
|
||||||
tempOutputFile = tempPdfWithoutImages;
|
|
||||||
}
|
|
||||||
// Read the OCR processed PDF file
|
|
||||||
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.delete(tempInputFile);
|
|
||||||
|
|
||||||
// Return the OCR processed PDF as a response
|
|
||||||
String outputFilename =
|
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_OCR.pdf";
|
|
||||||
|
|
||||||
if (sidecar != null && sidecar) {
|
|
||||||
// Create a zip file containing both the PDF and the text file
|
|
||||||
String outputZipFilename =
|
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_OCR.zip";
|
|
||||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
|
||||||
|
|
||||||
try (ZipOutputStream zipOut =
|
|
||||||
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
|
||||||
// Add PDF file to the zip
|
|
||||||
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
|
||||||
zipOut.putNextEntry(pdfEntry);
|
|
||||||
Files.copy(tempOutputFile, zipOut);
|
|
||||||
zipOut.closeEntry();
|
|
||||||
|
|
||||||
// Add text file to the zip
|
|
||||||
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
|
|
||||||
zipOut.putNextEntry(txtEntry);
|
|
||||||
Files.copy(sidecarTextPath, zipOut);
|
|
||||||
zipOut.closeEntry();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
// Remove images from the OCR processed PDF if the flag is set to true
|
||||||
|
if (removeImagesAfter != null && removeImagesAfter) {
|
||||||
|
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
|
||||||
|
|
||||||
// Clean up the temporary zip file
|
List<String> gsCommand =
|
||||||
Files.delete(tempZipFile);
|
Arrays.asList(
|
||||||
Files.delete(tempOutputFile);
|
"gs",
|
||||||
Files.delete(sidecarTextPath);
|
"-sDEVICE=pdfwrite",
|
||||||
|
"-dFILTERIMAGE",
|
||||||
|
"-o",
|
||||||
|
tempPdfWithoutImages.toString(),
|
||||||
|
tempOutputFile.toString());
|
||||||
|
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
|
.runCommandWithOutputHandling(gsCommand);
|
||||||
|
tempOutputFile = tempPdfWithoutImages;
|
||||||
|
}
|
||||||
|
// Read the OCR processed PDF file
|
||||||
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
|
|
||||||
// Return the zip file containing both the PDF and the text file
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
|
||||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
} else {
|
|
||||||
// Return the OCR processed PDF as a response
|
// Return the OCR processed PDF as a response
|
||||||
Files.delete(tempOutputFile);
|
String outputFilename =
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_OCR.pdf";
|
||||||
|
|
||||||
|
if (sidecar != null && sidecar) {
|
||||||
|
// Create a zip file containing both the PDF and the text file
|
||||||
|
String outputZipFilename =
|
||||||
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_OCR.zip";
|
||||||
|
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||||
|
|
||||||
|
try (ZipOutputStream zipOut =
|
||||||
|
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||||
|
// Add PDF file to the zip
|
||||||
|
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
||||||
|
zipOut.putNextEntry(pdfEntry);
|
||||||
|
Files.copy(tempOutputFile, zipOut);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
|
||||||
|
// Add text file to the zip
|
||||||
|
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
|
||||||
|
zipOut.putNextEntry(txtEntry);
|
||||||
|
Files.copy(sidecarTextPath, zipOut);
|
||||||
|
zipOut.closeEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||||
|
|
||||||
|
// Clean up the temporary zip file
|
||||||
|
Files.deleteIfExists(tempZipFile);
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
Files.deleteIfExists(sidecarTextPath);
|
||||||
|
|
||||||
|
// Return the zip file containing both the PDF and the text file
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
} else {
|
||||||
|
// Return the OCR processed PDF as a response
|
||||||
|
Files.deleteIfExists(tempOutputFile);
|
||||||
|
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,34 +41,35 @@ 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");
|
||||||
command.add("-o");
|
command.add("-o");
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
command.add("-sDEVICE=pdfwrite");
|
command.add("-sDEVICE=pdfwrite");
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||||
.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
|
// Return the optimized PDF as a response
|
||||||
Files.delete(tempInputFile);
|
String outputFilename =
|
||||||
Files.delete(tempOutputFile);
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
// Return the optimized PDF as a response
|
+ "_repaired.pdf";
|
||||||
String outputFilename =
|
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
} finally {
|
||||||
.replaceFirst("[.][^.]+$", "")
|
// Clean up the temporary files
|
||||||
+ "_repaired.pdf";
|
Files.deleteIfExists(tempInputFile);
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
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);
|
||||||
|
} finally {
|
||||||
|
if (tempFile != null) {
|
||||||
|
Files.deleteIfExists(tempFile.toPath());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
font = PDType0Font.load(document, tempFile);
|
|
||||||
tempFile.deleteOnExit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,7 +105,14 @@ public class PipelineProcessor {
|
|||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
body.add(entry.getKey(), entry.getValue());
|
if (entry.getValue() instanceof List) {
|
||||||
|
List<?> list = (List<?>) entry.getValue();
|
||||||
|
for (Object item : list) {
|
||||||
|
body.add(entry.getKey(), item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
@ -167,7 +174,14 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
body.add(entry.getKey(), entry.getValue());
|
if (entry.getValue() instanceof List) {
|
||||||
|
List<?> list = (List<?>) entry.getValue();
|
||||||
|
for (Object item : list) {
|
||||||
|
body.add(entry.getKey(), item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body.add(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
|
@ -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);
|
||||||
|
} finally {
|
||||||
|
if (tempFile != null) Files.deleteIfExists(tempFile.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
font = PDType0Font.load(document, tempFile);
|
|
||||||
tempFile.deleteOnExit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@ -21,6 +24,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
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.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;
|
||||||
@ -31,6 +39,7 @@ import stirling.software.SPDF.repository.UserRepository;
|
|||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
@ -38,6 +47,33 @@ public class AccountWebController {
|
|||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> providerList = new HashMap<>();
|
||||||
|
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth != null) {
|
||||||
|
if (oauth.isSettingsValid()) {
|
||||||
|
providerList.put("oidc", oauth.getProvider());
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client != null) {
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
if (google.isSettingsValid()) {
|
||||||
|
providerList.put(google.getName(), google.getClientName());
|
||||||
|
}
|
||||||
|
|
||||||
|
GithubProvider github = client.getGithub();
|
||||||
|
if (github.isSettingsValid()) {
|
||||||
|
providerList.put(github.getName(), github.getClientName());
|
||||||
|
}
|
||||||
|
|
||||||
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
|
if (keycloak.isSettingsValid()) {
|
||||||
|
providerList.put(keycloak.getName(), keycloak.getClientName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
model.addAttribute("providerlist", providerList);
|
||||||
|
|
||||||
model.addAttribute(
|
model.addAttribute(
|
||||||
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||||
|
|
||||||
@ -80,6 +116,21 @@ public class AccountWebController {
|
|||||||
break;
|
break;
|
||||||
case "invalid_token_response":
|
case "invalid_token_response":
|
||||||
erroroauth = "login.oauth2InvalidTokenResponse";
|
erroroauth = "login.oauth2InvalidTokenResponse";
|
||||||
|
break;
|
||||||
|
case "authorization_request_not_found":
|
||||||
|
erroroauth = "login.oauth2RequestNotFound";
|
||||||
|
break;
|
||||||
|
case "access_denied":
|
||||||
|
erroroauth = "login.oauth2AccessDenied";
|
||||||
|
break;
|
||||||
|
case "invalid_user_info_response":
|
||||||
|
erroroauth = "login.oauth2InvalidUserInfoResponse";
|
||||||
|
break;
|
||||||
|
case "invalid_request":
|
||||||
|
erroroauth = "login.oauth2invalidRequest";
|
||||||
|
break;
|
||||||
|
case "invalid_id_token":
|
||||||
|
erroroauth = "login.oauth2InvalidIdToken";
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -211,8 +262,7 @@ public class AccountWebController {
|
|||||||
userRepository.findByUsernameIgnoreCase(
|
userRepository.findByUsernameIgnoreCase(
|
||||||
username); // Assuming findByUsername method exists
|
username); // Assuming findByUsername method exists
|
||||||
if (!user.isPresent()) {
|
if (!user.isPresent()) {
|
||||||
// Handle error appropriately
|
return "redirect:/error";
|
||||||
return "redirect:/error"; // Example redirection in case of error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert settings map to JSON string
|
// Convert settings map to JSON string
|
||||||
@ -222,8 +272,8 @@ public class AccountWebController {
|
|||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
// Handle JSON conversion error
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
return "redirect:/error"; // Example redirection in case of error
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
|
@ -15,6 +15,8 @@ import java.util.Objects;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
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.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
@ -33,6 +35,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class GeneralWebController {
|
public class GeneralWebController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(GeneralWebController.class);
|
||||||
|
|
||||||
@GetMapping("/pipeline")
|
@GetMapping("/pipeline")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String pipelineForm(Model model) {
|
public String pipelineForm(Model model) {
|
||||||
@ -74,7 +78,7 @@ public class GeneralWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pipelineConfigsWithNames.size() == 0) {
|
if (pipelineConfigsWithNames.size() == 0) {
|
||||||
|
@ -6,6 +6,8 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
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.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@ -26,6 +28,8 @@ import stirling.software.SPDF.model.Dependency;
|
|||||||
@Controller
|
@Controller
|
||||||
public class HomeWebController {
|
public class HomeWebController {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(HomeWebController.class);
|
||||||
|
|
||||||
@GetMapping("/about")
|
@GetMapping("/about")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String gameForm(Model model) {
|
public String gameForm(Model model) {
|
||||||
@ -46,7 +50,7 @@ public class HomeWebController {
|
|||||||
mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {});
|
mapper.readValue(json, new TypeReference<Map<String, List<Dependency>>>() {});
|
||||||
model.addAttribute("dependencies", data.get("dependencies"));
|
model.addAttribute("dependencies", data.get("dependencies"));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("exception", e);
|
||||||
}
|
}
|
||||||
return "licenses";
|
return "licenses";
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,13 @@ public class SecurityWebController {
|
|||||||
return "security/cert-sign";
|
return "security/cert-sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/remove-cert-sign")
|
||||||
|
@Hidden
|
||||||
|
public String certUnSignForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "remove-cert-sign");
|
||||||
|
return "security/remove-cert-sign";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/sanitize-pdf")
|
@GetMapping("/sanitize-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String sanitizeForm(Model model) {
|
public String sanitizeForm(Model model) {
|
||||||
|
@ -6,6 +6,8 @@ import java.util.Collection;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
@ -23,6 +25,7 @@ public class ApplicationProperties {
|
|||||||
private Metrics metrics;
|
private Metrics metrics;
|
||||||
private AutomaticallyGenerated automaticallyGenerated;
|
private AutomaticallyGenerated automaticallyGenerated;
|
||||||
private AutoPipeline autoPipeline;
|
private AutoPipeline autoPipeline;
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
|
||||||
|
|
||||||
public AutoPipeline getAutoPipeline() {
|
public AutoPipeline getAutoPipeline() {
|
||||||
return autoPipeline != null ? autoPipeline : new AutoPipeline();
|
return autoPipeline != null ? autoPipeline : new AutoPipeline();
|
||||||
@ -182,13 +185,12 @@ public class ApplicationProperties {
|
|||||||
+ oauth2
|
+ oauth2
|
||||||
+ ", initialLogin="
|
+ ", initialLogin="
|
||||||
+ initialLogin
|
+ initialLogin
|
||||||
+ ", csrfDisabled="
|
+ ", csrfDisabled="
|
||||||
+ csrfDisabled
|
+ csrfDisabled
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class InitialLogin {
|
public static class InitialLogin {
|
||||||
|
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@ -219,22 +221,21 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class OAUTH2 {
|
public static class OAUTH2 {
|
||||||
|
private Boolean enabled = false;
|
||||||
private boolean enabled;
|
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private boolean autoCreateUser;
|
private Boolean autoCreateUser = false;
|
||||||
private String useAsUsername;
|
private String useAsUsername;
|
||||||
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
private String provider;
|
private String provider;
|
||||||
|
private Client client = new Client();
|
||||||
|
|
||||||
private Collection<String> scopes = new ArrayList<String>();
|
public Boolean getEnabled() {
|
||||||
|
|
||||||
public boolean getEnabled() {
|
|
||||||
return enabled;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEnabled(boolean enabled) {
|
public void setEnabled(Boolean enabled) {
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,19 +263,16 @@ public class ApplicationProperties {
|
|||||||
this.clientSecret = clientSecret;
|
this.clientSecret = clientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getAutoCreateUser() {
|
public Boolean getAutoCreateUser() {
|
||||||
return autoCreateUser;
|
return autoCreateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAutoCreateUser(boolean autoCreateUser) {
|
public void setAutoCreateUser(Boolean autoCreateUser) {
|
||||||
this.autoCreateUser = autoCreateUser;
|
this.autoCreateUser = autoCreateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUseAsUsername() {
|
public String getUseAsUsername() {
|
||||||
if (useAsUsername != null && useAsUsername.trim().length() > 0) {
|
return useAsUsername;
|
||||||
return useAsUsername;
|
|
||||||
}
|
|
||||||
return "email";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUseAsUsername(String useAsUsername) {
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
@ -293,14 +291,44 @@ public class ApplicationProperties {
|
|||||||
return scopes;
|
return scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScopes(String scpoes) {
|
public void setScopes(String scopes) {
|
||||||
List<String> scopesList =
|
List<String> scopesList =
|
||||||
Arrays.stream(scpoes.split(","))
|
Arrays.stream(scopes.split(","))
|
||||||
.map(String::trim)
|
.map(String::trim)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
this.scopes.addAll(scopesList);
|
this.scopes.addAll(scopesList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Client getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClient(Client client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isValid(String value, String name) {
|
||||||
|
if (value != null && !value.trim().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isValid(Collection<String> value, String name) {
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSettingsValid() {
|
||||||
|
return isValid(this.getIssuer(), "issuer")
|
||||||
|
&& isValid(this.getClientId(), "clientId")
|
||||||
|
&& isValid(this.getClientSecret(), "clientSecret")
|
||||||
|
&& isValid(this.getScopes(), "scopes")
|
||||||
|
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "OAUTH2 [enabled="
|
return "OAUTH2 [enabled="
|
||||||
@ -315,12 +343,372 @@ public class ApplicationProperties {
|
|||||||
+ autoCreateUser
|
+ autoCreateUser
|
||||||
+ ", useAsUsername="
|
+ ", useAsUsername="
|
||||||
+ useAsUsername
|
+ useAsUsername
|
||||||
+ ", provider"
|
+ ", provider="
|
||||||
+ provider
|
+ provider
|
||||||
+ ", scopes="
|
+ ", scopes="
|
||||||
+ scopes
|
+ scopes
|
||||||
+ "]";
|
+ "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Client {
|
||||||
|
private GoogleProvider google = new GoogleProvider();
|
||||||
|
private GithubProvider github = new GithubProvider();
|
||||||
|
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||||
|
|
||||||
|
public Provider get(String registrationId) throws Exception {
|
||||||
|
switch (registrationId.toLowerCase()) {
|
||||||
|
case "google":
|
||||||
|
return getGoogle();
|
||||||
|
case "github":
|
||||||
|
return getGithub();
|
||||||
|
case "keycloak":
|
||||||
|
return getKeycloak();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
throw new Exception("Provider not supported, use custom setting.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public GoogleProvider getGoogle() {
|
||||||
|
return google;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGoogle(GoogleProvider google) {
|
||||||
|
this.google = google;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GithubProvider getGithub() {
|
||||||
|
return github;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setGithub(GithubProvider github) {
|
||||||
|
this.github = github;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeycloakProvider getKeycloak() {
|
||||||
|
return keycloak;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeycloak(KeycloakProvider keycloak) {
|
||||||
|
this.keycloak = keycloak;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Client [google="
|
||||||
|
+ google
|
||||||
|
+ ", github="
|
||||||
|
+ github
|
||||||
|
+ ", keycloak="
|
||||||
|
+ keycloak
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GoogleProvider extends Provider {
|
||||||
|
|
||||||
|
private static final String authorizationUri =
|
||||||
|
"https://accounts.google.com/o/oauth2/v2/auth";
|
||||||
|
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
|
||||||
|
private static final String userInfoUri =
|
||||||
|
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
|
||||||
|
|
||||||
|
public String getAuthorizationuri() {
|
||||||
|
return authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenuri() {
|
||||||
|
return tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserinfouri() {
|
||||||
|
return userInfoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
|
private String useAsUsername = "email";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getScopes() {
|
||||||
|
if (scopes == null || scopes.isEmpty()) {
|
||||||
|
scopes = new ArrayList<>();
|
||||||
|
scopes.add("https://www.googleapis.com/auth/userinfo.email");
|
||||||
|
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
|
||||||
|
}
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScopes(String scopes) {
|
||||||
|
this.scopes =
|
||||||
|
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUseAsUsername() {
|
||||||
|
return this.useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
|
this.useAsUsername = useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Google [clientId="
|
||||||
|
+ clientId
|
||||||
|
+ ", clientSecret="
|
||||||
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
|
+ ", scopes="
|
||||||
|
+ scopes
|
||||||
|
+ ", useAsUsername="
|
||||||
|
+ useAsUsername
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "google";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientName() {
|
||||||
|
return "Google";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSettingsValid() {
|
||||||
|
return super.isValid(this.getClientId(), "clientId")
|
||||||
|
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||||
|
&& super.isValid(this.getScopes(), "scopes")
|
||||||
|
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GithubProvider extends Provider {
|
||||||
|
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
|
||||||
|
private static final String tokenUri = "https://github.com/login/oauth/access_token";
|
||||||
|
private static final String userInfoUri = "https://api.github.com/user";
|
||||||
|
|
||||||
|
public String getAuthorizationuri() {
|
||||||
|
return authorizationUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTokenuri() {
|
||||||
|
return tokenUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserinfouri() {
|
||||||
|
return userInfoUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
|
private String useAsUsername = "login";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIssuer() {
|
||||||
|
return new String();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIssuer(String issuer) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getScopes() {
|
||||||
|
if (scopes == null || scopes.isEmpty()) {
|
||||||
|
scopes = new ArrayList<>();
|
||||||
|
scopes.add("read:user");
|
||||||
|
}
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScopes(String scopes) {
|
||||||
|
this.scopes =
|
||||||
|
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUseAsUsername() {
|
||||||
|
return this.useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
|
this.useAsUsername = useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GitHub [clientId="
|
||||||
|
+ clientId
|
||||||
|
+ ", clientSecret="
|
||||||
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
|
+ ", scopes="
|
||||||
|
+ scopes
|
||||||
|
+ ", useAsUsername="
|
||||||
|
+ useAsUsername
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "github";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientName() {
|
||||||
|
return "GitHub";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSettingsValid() {
|
||||||
|
return super.isValid(this.getClientId(), "clientId")
|
||||||
|
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||||
|
&& super.isValid(this.getScopes(), "scopes")
|
||||||
|
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class KeycloakProvider extends Provider {
|
||||||
|
private String issuer;
|
||||||
|
private String clientId;
|
||||||
|
private String clientSecret;
|
||||||
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
|
private String useAsUsername = "email";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIssuer() {
|
||||||
|
return this.issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
this.issuer = issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
this.clientId = clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getScopes() {
|
||||||
|
if (scopes == null || scopes.isEmpty()) {
|
||||||
|
scopes = new ArrayList<>();
|
||||||
|
scopes.add("profile");
|
||||||
|
scopes.add("email");
|
||||||
|
}
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScopes(String scopes) {
|
||||||
|
this.scopes =
|
||||||
|
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUseAsUsername() {
|
||||||
|
return this.useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
|
this.useAsUsername = useAsUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Keycloak [issuer="
|
||||||
|
+ issuer
|
||||||
|
+ ", clientId="
|
||||||
|
+ clientId
|
||||||
|
+ ", clientSecret="
|
||||||
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
|
+ ", scopes="
|
||||||
|
+ scopes
|
||||||
|
+ ", useAsUsername="
|
||||||
|
+ useAsUsername
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "keycloak";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientName() {
|
||||||
|
return "Keycloak";
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSettingsValid() {
|
||||||
|
return isValid(this.getIssuer(), "issuer")
|
||||||
|
&& isValid(this.getClientId(), "clientId")
|
||||||
|
&& isValid(this.getClientSecret(), "clientSecret")
|
||||||
|
&& isValid(this.getScopes(), "scopes")
|
||||||
|
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,9 +769,6 @@ public class ApplicationProperties {
|
|||||||
this.googlevisibility = googlevisibility;
|
this.googlevisibility = googlevisibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "System [defaultLocale="
|
return "System [defaultLocale="
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
|
||||||
|
import org.thymeleaf.templateresource.ITemplateResource;
|
||||||
|
|
||||||
|
public class InputStreamTemplateResource implements ITemplateResource {
|
||||||
|
private InputStream inputStream;
|
||||||
|
private String characterEncoding;
|
||||||
|
|
||||||
|
public InputStreamTemplateResource(InputStream inputStream, String characterEncoding) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
this.characterEncoding = characterEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Reader reader() throws IOException {
|
||||||
|
return new InputStreamReader(inputStream, characterEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ITemplateResource relative(String relativeLocation) {
|
||||||
|
// Implement logic for relative resources, if needed
|
||||||
|
throw new UnsupportedOperationException("Relative resources not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "InputStream resource [Stream]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getBaseName() {
|
||||||
|
return "streamResource";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean exists() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
92
src/main/java/stirling/software/SPDF/model/Provider.java
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class Provider implements ProviderInterface {
|
||||||
|
private String name;
|
||||||
|
private String clientName;
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientName() {
|
||||||
|
return clientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isValid(String value, String name) {
|
||||||
|
if (value != null && !value.trim().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isValid(Collection<String> value, String name) {
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<String> getScopes() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setScopes(String scopes) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUseAsUsername() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIssuer() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIssuer(String issuer) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientSecret() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientSecret(String clientSecret) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getClientId() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setClientId(String clientId) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public interface ProviderInterface {
|
||||||
|
|
||||||
|
public Collection<String> getScopes();
|
||||||
|
|
||||||
|
public void setScopes(String scopes);
|
||||||
|
|
||||||
|
public String getUseAsUsername();
|
||||||
|
|
||||||
|
public void setUseAsUsername(String useAsUsername);
|
||||||
|
|
||||||
|
public String getIssuer();
|
||||||
|
|
||||||
|
public void setIssuer(String issuer);
|
||||||
|
|
||||||
|
public String getClientSecret();
|
||||||
|
|
||||||
|
public void setClientSecret(String clientSecret);
|
||||||
|
|
||||||
|
public String getClientId();
|
||||||
|
|
||||||
|
public void setClientId(String clientId);
|
||||||
|
}
|