Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5564a6e730 | ||
|
|
8a58647ffd | ||
|
|
37dcae282a | ||
|
|
58618b3a21 | ||
|
|
4a4c7faf47 | ||
|
|
66324a5bdc | ||
|
|
3bd18f7c5e | ||
|
|
e693bbb2bd | ||
|
|
f11ad92fa5 | ||
|
|
f443a4e0de | ||
|
|
fa0152aa2d | ||
|
|
e1d0f2cd3e | ||
|
|
81e2a77e57 | ||
|
|
6c9a4e8acc | ||
|
|
8e5b3ea7f1 | ||
|
|
b47f8a2c17 | ||
|
|
56a07bbf3a | ||
|
|
987d793ef4 | ||
|
|
ea85d76f5b | ||
|
|
fc762329a8 | ||
|
|
48bae227f6 | ||
|
|
9773138612 | ||
|
|
1927801894 | ||
|
|
3cd6f462a8 | ||
|
|
6dab3980fc | ||
|
|
ea2d755808 | ||
|
|
05efcedea8 | ||
|
|
29fcbf30d7 | ||
|
|
2cbe34ea24 | ||
|
|
29f43c010e | ||
|
|
8602f38fbf | ||
|
|
f5258c593b | ||
|
|
e89ac84928 | ||
|
|
8997855922 | ||
|
|
09c93cebe3 | ||
|
|
00d65596d1 | ||
|
|
851b43fadf | ||
|
|
d5ac560452 | ||
|
|
4ea323b879 | ||
|
|
5c84ae1b53 | ||
|
|
96a8898c15 | ||
|
|
1cca13334e | ||
|
|
264763dd4c | ||
|
|
eafbfb8dbf | ||
|
|
6fa7c2e5e1 | ||
|
|
909054a49d | ||
|
|
711501a382 | ||
|
|
e45b512087 | ||
|
|
d32da95f55 | ||
|
|
b54d73d723 | ||
|
|
503a1c9526 | ||
|
|
f176558a39 | ||
|
|
68c387086c | ||
|
|
f165439d26 | ||
|
|
6649ffd7a0 | ||
|
|
8dbbacb09e | ||
|
|
908b409155 | ||
|
|
4ad716f281 | ||
|
|
148feda83f | ||
|
|
771b312ee8 | ||
|
|
00a0670954 | ||
|
|
39423c247c | ||
|
|
6d8d0bad56 | ||
|
|
a3374745f8 | ||
|
|
d65a637a46 | ||
|
|
d0bf385d69 | ||
|
|
bc35745768 | ||
|
|
e50391a44a | ||
|
|
96b080528b | ||
|
|
f35cbc4310 | ||
|
|
c09fc1541f | ||
|
|
dff53310a7 | ||
|
|
ec537c6fde | ||
|
|
ce70796fff | ||
|
|
7db7192d95 | ||
|
|
d00e7fe958 | ||
|
|
510f39ad41 | ||
|
|
950a0c4b21 | ||
|
|
e6793bd04a | ||
|
|
0f60974a57 | ||
|
|
0ed4c16dc0 | ||
|
|
ea6d4a293e | ||
|
|
191e79da18 | ||
|
|
c54c18b247 | ||
|
|
39cbb5e7d9 | ||
|
|
3df0474ed2 | ||
|
|
9ff2cb63d0 | ||
|
|
d8087d8c55 | ||
|
|
0dfb4d77c0 | ||
|
|
065f53e577 | ||
|
|
c899f605a9 | ||
|
|
47de0f84db | ||
|
|
543b96c033 | ||
|
|
c1126e57bd | ||
|
|
7c5077006d | ||
|
|
3e7889cee8 | ||
|
|
281047f42a | ||
|
|
07f85ea8b4 | ||
|
|
e07f73dce7 | ||
|
|
bfe38c71e8 | ||
|
|
072090d41b | ||
|
|
560936e182 | ||
|
|
6eb79e65fa | ||
|
|
cbe92269f4 | ||
|
|
81871a6f10 | ||
|
|
cf2a7896da | ||
|
|
6a3d95ba09 | ||
|
|
85ed0c38d1 | ||
|
|
6c7dc34640 | ||
|
|
ecfdfa5644 | ||
|
|
11e279bd12 | ||
|
|
929f0bbbe5 | ||
|
|
5751b1ac2d | ||
|
|
4bf78ffd5d | ||
|
|
b7d37deb85 |
14
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
14
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
@@ -11,6 +11,20 @@ body:
|
|||||||
|
|
||||||
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: installation-method
|
||||||
|
attributes:
|
||||||
|
label: Installation Method
|
||||||
|
description: |
|
||||||
|
Indicate whether you are using Docker or a local installation.
|
||||||
|
options:
|
||||||
|
- Docker
|
||||||
|
- Docker ultra lite
|
||||||
|
- Docker fat
|
||||||
|
- Local Installation
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
@@ -1,6 +1,8 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Submit a new feature request.
|
description: Submit a new feature request.
|
||||||
title: "[Feature Request]: "
|
title: "[Feature Request]: "
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -9,6 +9,8 @@ updates:
|
|||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
rebase-strategy: "auto"
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/" # Location of Dockerfile
|
directory: "/" # Location of Dockerfile
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
44
.github/labeler-config.yml
vendored
44
.github/labeler-config.yml
vendored
@@ -1,20 +1,54 @@
|
|||||||
translation:
|
Translation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
||||||
|
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
|
||||||
|
|
||||||
Front End:
|
Front End:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/resources/templates/**'
|
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/static/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
||||||
|
|
||||||
java:
|
Java:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
||||||
|
|
||||||
documentation:
|
Back End:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
|
||||||
|
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
|
||||||
|
|
||||||
|
API:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||||
|
|
||||||
|
Documentation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: '**/*.md'
|
- any-glob-to-any-file: '**/*.md'
|
||||||
|
- any-glob-to-any-file: 'scripts/counter_translation.py'
|
||||||
|
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
||||||
|
|
||||||
docker:
|
Docker:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'Dockerfile'
|
- any-glob-to-any-file: 'Dockerfile'
|
||||||
- any-glob-to-any-file: 'Dockerfile-*'
|
- any-glob-to-any-file: 'Dockerfile-*'
|
||||||
|
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
||||||
|
|
||||||
|
Test:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: 'cucumber/**/*'
|
||||||
|
- any-glob-to-any-file: 'src/test**/*'
|
||||||
|
|
||||||
|
Github:
|
||||||
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: '.github/**/*'
|
||||||
|
|||||||
93
.github/labels.yml
vendored
Normal file
93
.github/labels.yml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Labels names are important as they are used by Release Drafter to decide
|
||||||
|
# regarding where to record them in changelog or if to skip them.
|
||||||
|
#
|
||||||
|
# The repository labels will be automatically configured using this file and
|
||||||
|
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
||||||
|
- name: "Back End"
|
||||||
|
color: "20CE6C"
|
||||||
|
description: "Issues related to back-end development"
|
||||||
|
from_name: "Back end"
|
||||||
|
- name: "Bug"
|
||||||
|
description: "Something isn't working"
|
||||||
|
color: "EB9CA6"
|
||||||
|
from_name: "bug"
|
||||||
|
- name: "dependencies"
|
||||||
|
description: "Pull requests that update a dependency file"
|
||||||
|
color: "5AA8FC"
|
||||||
|
- name: "Docker"
|
||||||
|
description: "Pull requests that update Docker code"
|
||||||
|
color: "1FCEFF"
|
||||||
|
from_name: "docker"
|
||||||
|
- name: "Documentation"
|
||||||
|
description: "Improvements or additions to documentation"
|
||||||
|
color: "35ABFF"
|
||||||
|
from_name: "documentation"
|
||||||
|
- name: "Done for next release"
|
||||||
|
color: "0CDBD1"
|
||||||
|
- name: "Done"
|
||||||
|
color: "60F13B"
|
||||||
|
- name: "duplicate"
|
||||||
|
description: "This issue or pull request already exists"
|
||||||
|
color: "CDD1D5"
|
||||||
|
- name: "enhancement"
|
||||||
|
description: "New feature or request"
|
||||||
|
color: "A0EEEE"
|
||||||
|
- name: "fix needs confirmation"
|
||||||
|
color: "60A1E7"
|
||||||
|
description: "Fix needs to be confirmed"
|
||||||
|
- name: "Front End"
|
||||||
|
color: "BBD2F1"
|
||||||
|
description: "Issues related to front-end development"
|
||||||
|
- name: "github-actions"
|
||||||
|
description: "Pull requests that update GitHub Actions code"
|
||||||
|
color: "999999"
|
||||||
|
from_name: "github_actions"
|
||||||
|
- name: "good first issue"
|
||||||
|
description: "Good for newcomers"
|
||||||
|
color: "C1B8FF"
|
||||||
|
- name: "help wanted"
|
||||||
|
description: "Extra attention is needed"
|
||||||
|
color: "00E6C4"
|
||||||
|
- name: "invalid"
|
||||||
|
description: "This doesn't seem right"
|
||||||
|
color: "E5E566"
|
||||||
|
- name: "Java"
|
||||||
|
description: "Pull requests that update Java code"
|
||||||
|
color: "FF9E1F"
|
||||||
|
from_name: "java"
|
||||||
|
- name: "Long-term Enhancement"
|
||||||
|
color: "BFDEC3"
|
||||||
|
description: "Enhancements planned for the long term"
|
||||||
|
- name: "more-info-needed"
|
||||||
|
color: "00E4F8"
|
||||||
|
description: "More information is needed"
|
||||||
|
- name: "needs investigation"
|
||||||
|
color: "B8C3A7"
|
||||||
|
description: "Issues that require further investigation"
|
||||||
|
- name: "Prioritised enhancement"
|
||||||
|
color: "4BA2EE"
|
||||||
|
description: "High-priority enhancements"
|
||||||
|
- name: "question"
|
||||||
|
description: "Further information is requested"
|
||||||
|
color: "D97EE5"
|
||||||
|
- name: "Translation"
|
||||||
|
color: "9FABF9"
|
||||||
|
from_name: "translation"
|
||||||
|
- name: "upstream"
|
||||||
|
color: "DEDEDE"
|
||||||
|
- name: "v2"
|
||||||
|
color: "FFFF00"
|
||||||
|
- name: "wontfix"
|
||||||
|
description: "This will not be worked on"
|
||||||
|
color: "FFFFFF"
|
||||||
|
- name: "Security"
|
||||||
|
color: "000000"
|
||||||
|
description: "Security-related issues or pull requests"
|
||||||
|
- name: "API"
|
||||||
|
color: "FFFF00"
|
||||||
|
description: "API-related issues or pull requests"
|
||||||
|
- name: "Test"
|
||||||
|
color: "FF9E1F"
|
||||||
|
description: "Testing-related issues or pull requests"
|
||||||
|
- name: "Stale"
|
||||||
|
color: "000000"
|
||||||
1
.github/scripts/check_tabulator.py
vendored
1
.github/scripts/check_tabulator.py
vendored
@@ -1,4 +1,5 @@
|
|||||||
"""check_tabulator.py"""
|
"""check_tabulator.py"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/labeler@v5
|
|
||||||
|
- name: Apply Labels
|
||||||
|
uses: actions/labeler@v5
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
configuration-path: .github/labeler-config.yml
|
configuration-path: .github/labeler-config.yml
|
||||||
60
.github/workflows/build.yml
vendored
60
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: "Build repo"
|
name: Build repo
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,20 +17,72 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
jdk-version: [17, 21]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK ${{ matrix.jdk-version }}
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: "17"
|
java-version: ${{ matrix.jdk-version }}
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- name: Set up Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --no-build-cache
|
run: ./gradlew build --no-build-cache
|
||||||
|
|
||||||
|
docker-compose-tests:
|
||||||
|
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||||
|
# (github.event_name == 'pull_request' &&
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
|
||||||
|
# (
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Java') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Security') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'API') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
|
||||||
|
# contains(github.event.pull_request.labels.*.name, 'Test')
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "adopt"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install Docker Compose
|
||||||
|
run: |
|
||||||
|
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.7"
|
||||||
|
|
||||||
|
- name: Pip requirements
|
||||||
|
run: |
|
||||||
|
pip install -r ./cucumber/requirements.txt
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
|
run: |
|
||||||
|
chmod +x ./test.sh
|
||||||
|
./test.sh
|
||||||
|
|||||||
19
.github/workflows/licenses-update.yml
vendored
19
.github/workflows/licenses-update.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean generateLicenseReport
|
run: ./gradlew clean generateLicenseReport
|
||||||
@@ -45,6 +45,7 @@ jobs:
|
|||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
@@ -57,6 +58,22 @@ jobs:
|
|||||||
title: "Update 3rd Party Licenses"
|
title: "Update 3rd Party Licenses"
|
||||||
body: |
|
body: |
|
||||||
Auto-generated by [create-pull-request][1]
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
|
labels: licenses
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
|
||||||
|
- name: Auto approve
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
run: gh pr review --approve "${{ steps.cpr.outputs.pull-request-number }}"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Enable auto-merge
|
||||||
|
if: steps.cpr.outputs.pull-request-operation == 'created'
|
||||||
|
uses: peter-evans/enable-pull-request-automerge@v3
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
||||||
|
merge-method: squash # Choose the merge method: merge, squash, or rebase
|
||||||
|
|||||||
24
.github/workflows/manage-label.yml
vendored
Normal file
24
.github/workflows/manage-label.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
name: Manage labels
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 20 * * *"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
labeler:
|
||||||
|
name: Labeler
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Labeler
|
||||||
|
uses: crazy-max/ghaction-github-labeler@v5
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
yaml-file: .github/labels.yml
|
||||||
|
skip-delete: true
|
||||||
9
.github/workflows/push-docker.yml
vendored
9
.github/workflows/push-docker.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -111,7 +111,6 @@ jobs:
|
|||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -125,7 +124,7 @@ jobs:
|
|||||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
|||||||
2
.github/workflows/releaseArtifacts.yml
vendored
2
.github/workflows/releaseArtifacts.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v3
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|||||||
2
.github/workflows/sync_files.yml
vendored
2
.github/workflows/sync_files.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
|||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
labels: github-actions
|
||||||
sync-readme:
|
sync-readme:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -88,3 +89,4 @@ jobs:
|
|||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
labels: Documentation,Translation,github-actions
|
||||||
|
|||||||
47
.github/workflows/test.yml
vendored
47
.github/workflows/test.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Docker Compose Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "src/**"
|
|
||||||
- "**.gradle"
|
|
||||||
- "!src/main/java/resources/messages*"
|
|
||||||
- "exampleYmlFiles/**"
|
|
||||||
- "Dockerfile"
|
|
||||||
- "Dockerfile**"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Java 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "adopt"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Install Docker Compose
|
|
||||||
run: |
|
|
||||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
# sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.7"
|
|
||||||
|
|
||||||
- name: Pip requirements
|
|
||||||
run: |
|
|
||||||
pip install -r ./cucumber/requirements.txt
|
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
|
||||||
run: |
|
|
||||||
chmod +x ./test.sh
|
|
||||||
./test.sh
|
|
||||||
43
.gitignore
vendored
43
.gitignore
vendored
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
### Eclipse ###
|
### Eclipse ###
|
||||||
.metadata
|
.metadata
|
||||||
bin/
|
bin/
|
||||||
@@ -22,7 +20,6 @@ customFiles/
|
|||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
.lock
|
.lock
|
||||||
@@ -119,8 +116,28 @@ watchedFolders/
|
|||||||
*.db
|
*.db
|
||||||
/build
|
/build
|
||||||
|
|
||||||
/.vscode
|
# Byte-compiled / optimized / DLL files
|
||||||
/.idea
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env*
|
||||||
|
.venv*
|
||||||
|
env*/
|
||||||
|
venv*/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
/.vscode/**/*
|
||||||
|
!/.vscode/settings.json
|
||||||
|
|
||||||
|
# IntelliJ IDEA
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
out/
|
||||||
|
|
||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -128,3 +145,19 @@ watchedFolders/
|
|||||||
|
|
||||||
# cucumber
|
# cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
|
# Certs
|
||||||
|
*.p12
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
*.cer
|
||||||
|
*.der
|
||||||
|
*.key
|
||||||
|
*.csr
|
||||||
|
|
||||||
|
# cache
|
||||||
|
.ruff_cache
|
||||||
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
|||||||
53
.vscode/settings.json
vendored
Normal file
53
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"files.eol": "auto",
|
||||||
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
|
"black-formatter.args": ["--line-length", "127"],
|
||||||
|
"flake8.args": ["--max-line-length", "127"],
|
||||||
|
"pylint.args": ["max-line-length", "127"],
|
||||||
|
"[java]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[gradle-build]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[gradle]": {
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.rulers": [127],
|
||||||
|
"files.trimFinalNewlines": false,
|
||||||
|
"files.insertFinalNewline": false
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"editor.rulers": [127]
|
||||||
|
},
|
||||||
|
"[yaml]": {
|
||||||
|
"files.trimFinalNewlines": false,
|
||||||
|
"files.insertFinalNewline": false
|
||||||
|
},
|
||||||
|
"diffEditor.maxComputationTime": 0,
|
||||||
|
"editor.wordSegmenterLocales": null,
|
||||||
|
"editor.guides.bracketPairs": "active",
|
||||||
|
"editor.guides.bracketPairsHorizontal": "active",
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"files.trimFinalNewlines": true,
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"editor.indentSize": "tabSize",
|
||||||
|
"editor.stickyScroll.enabled": false,
|
||||||
|
"editor.minimap.enabled": false,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -39,16 +39,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
libreoffice \
|
libreoffice \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 && \
|
python3 \
|
||||||
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
py3-pip && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
|
|||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -31,7 +31,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
PGID=1000 \
|
PGID=1000 \
|
||||||
UMASK=022 \
|
UMASK=022 \
|
||||||
FAT_DOCKER=true \
|
FAT_DOCKER=true \
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=true
|
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
|
||||||
|
|
||||||
|
|
||||||
# JDK for app
|
# JDK for app
|
||||||
@@ -45,7 +45,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
calibre@testing \
|
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
@@ -62,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 && \
|
python3 \
|
||||||
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
py3-pip && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.20.0
|
FROM alpine:3.20.2
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
|||||||
65
README.md
65
README.md
@@ -165,42 +165,46 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
|
|||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling PDF currently supports 32!
|
Stirling PDF currently supports 38!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| ------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
|
| Catalan (Català) (ca_CA) |  |
|
||||||
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
|
| Danish (Dansk) (da_DK) |  |
|
||||||
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
|
||||||
| Korean (한국어) (ko_KR) |  |
|
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
|
||||||
| Russian (Русский) (ru_RU) |  |
|
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
|
||||||
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
|
||||||
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
|
| Polish (Polski) (pl_PL) |  |
|
||||||
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
|
| Romanian (Română) (ro_RO) |  |
|
||||||
|
| Russian (Русский) (ru_RU) |  |
|
||||||
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
|
| Spanish (Español) (es_ES) |  |
|
||||||
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
|
| Thai (ไทย) (th_TH) |  |
|
||||||
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
@@ -262,6 +266,7 @@ security:
|
|||||||
clientId: '' # Client ID from your provider
|
clientId: '' # Client ID from your provider
|
||||||
clientSecret: '' # Client Secret from your provider
|
clientSecret: '' # Client Secret from your provider
|
||||||
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
|
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
||||||
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
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'
|
||||||
|
|||||||
84
build.gradle
84
build.gradle
@@ -1,25 +1,32 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.3.0"
|
id "org.springframework.boot" version "3.3.2"
|
||||||
id "io.spring.dependency-management" version "1.1.5"
|
id "io.spring.dependency-management" version "1.1.6"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id "edu.sc.seis.launch4j" version "3.0.5"
|
id "edu.sc.seis.launch4j" version "3.0.6"
|
||||||
id "com.diffplug.spotless" version "6.25.0"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.8"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.3.0"
|
springBootVersion = "3.3.2"
|
||||||
|
pdfboxVersion = "3.0.3"
|
||||||
|
logbackVersion = "1.5.7"
|
||||||
|
imageioVersion = "3.11.0"
|
||||||
|
lombokVersion = "1.18.34"
|
||||||
|
bouncycastleVersion = "1.78.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.26.1"
|
version = "0.28.2"
|
||||||
|
|
||||||
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
sourceCompatibility = "17"
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
@@ -40,8 +47,10 @@ sourceSets {
|
|||||||
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||||
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||||
|
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
||||||
exclude "stirling/software/SPDF/model/Authority.java"
|
exclude "stirling/software/SPDF/model/Authority.java"
|
||||||
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
||||||
|
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
exclude "stirling/software/SPDF/repository/**"
|
exclude "stirling/software/SPDF/repository/**"
|
||||||
}
|
}
|
||||||
@@ -97,11 +106,11 @@ tasks.wrapper {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
//security updates
|
//security updates
|
||||||
implementation "ch.qos.logback:logback-classic:1.5.6"
|
implementation "ch.qos.logback:logback-classic:$logbackVersion"
|
||||||
implementation "ch.qos.logback:logback-core:1.5.6"
|
implementation "ch.qos.logback:logback-core:$logbackVersion"
|
||||||
implementation "org.springframework:spring-webmvc:6.1.9"
|
implementation "org.springframework:spring-webmvc:6.1.9"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.1.3")
|
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||||
|
|
||||||
// implementation "org.yaml:snakeyaml:2.2"
|
// implementation "org.yaml:snakeyaml:2.2"
|
||||||
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
||||||
@@ -131,22 +140,22 @@ dependencies {
|
|||||||
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
||||||
|
|
||||||
// TwelveMonkeys
|
// TwelveMonkeys
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-batik:3.10.1"
|
implementation "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-bmp:3.10.1"
|
implementation "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-hdr:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-icns:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-iff:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-jpeg:3.11.0"
|
implementation "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pcx:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pict:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-pnm:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-psd:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-sgi:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-tga:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-tiff:3.10.1"
|
implementation "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
|
||||||
implementation "com.twelvemonkeys.imageio:imageio-webp:3.10.1"
|
implementation "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
||||||
// implementation "com.twelvemonkeys.imageio:imageio-xwd:3.10.1"
|
// implementation "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
||||||
|
|
||||||
implementation "commons-io:commons-io:2.16.1"
|
implementation "commons-io:commons-io:2.16.1"
|
||||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||||
@@ -158,33 +167,36 @@ dependencies {
|
|||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:pdfbox:3.0.2") {
|
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:xmpbox:3.0.2") {
|
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
|
|
||||||
|
|
||||||
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
||||||
|
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:1.78.1"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:1.78.1"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
implementation "io.micrometer:micrometer-core:1.13.0"
|
implementation "io.micrometer:micrometer-core:1.13.3"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.22.0"
|
implementation "org.commonmark:commonmark:0.22.0"
|
||||||
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
||||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||||
implementation "com.bucket4j:bucket4j_jdk17-core:8.12.1"
|
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
|
||||||
|
|
||||||
implementation "com.fathzer:javaluator:3.0.4"
|
implementation "com.fathzer:javaluator:3.0.4"
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||||
compileOnly "org.projectlombok:lombok:1.18.32"
|
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
||||||
annotationProcessor "org.projectlombok:lombok:1.18.32"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
testImplementation 'org.mockito:mockito-inline:3.12.4'
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.26.1
|
appVersion: 0.28.2
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ spec:
|
|||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
||||||
{{- if .Values.envs }}
|
|
||||||
env:
|
env:
|
||||||
|
- name: SYSTEM_ROOTURIPATH
|
||||||
|
value: {{ .Values.rootPath}}
|
||||||
|
{{- if .Values.envs }}
|
||||||
{{ toYaml .Values.envs | indent 8 }}
|
{{ toYaml .Values.envs | indent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.extraArgs }}
|
{{- if .Values.extraArgs }}
|
||||||
@@ -75,13 +77,13 @@ spec:
|
|||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: {{ .Values.rootPath}}
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.liveness | indent 10 }}
|
{{ toYaml .Values.probes.liveness | indent 10 }}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: {{ .Values.rootPath}}
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.readiness | indent 10 }}
|
{{ toYaml .Values.probes.readiness | indent 10 }}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ secret:
|
|||||||
commonLabels: {}
|
commonLabels: {}
|
||||||
# team_name: dev
|
# team_name: dev
|
||||||
|
|
||||||
|
# rootpath for the application
|
||||||
|
rootPath: /
|
||||||
|
|
||||||
envs: []
|
envs: []
|
||||||
# - name: UI_APP_NAME
|
# - name: UI_APP_NAME
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
@@ -24,8 +27,6 @@ envs: []
|
|||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
# - name: ALLOW_GOOGLE_VISIBILITY
|
# - name: ALLOW_GOOGLE_VISIBILITY
|
||||||
# value: "true"
|
# value: "true"
|
||||||
# - name: APP_ROOT_PATH
|
|
||||||
# value: "/"
|
|
||||||
# - name: APP_LOCALE
|
# - name: APP_LOCALE
|
||||||
# value: "en_GB"
|
# value: "en_GB"
|
||||||
|
|
||||||
|
|||||||
106
cucumber/exampleFiles/ghost1.pdf
Normal file
106
cucumber/exampleFiles/ghost1.pdf
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@Gb79+X'F"5[`EfJOD4:mD<%*=m+N>oDG,>NK`<U'B^0WYY,dWl^i_UcRk`<"L=<NPC$BtQ<5l$3<Y!?BuoCSYQ6GSt25lpqr0IrP?S[b)9%M"e'HHFqcRO'9eRaR0'DYi*Y.:nEMFAoTM;rPL%EF]`CfoELVl_Q,"LS:%iI;Nc[&bG.*65O]ecfK1'*<>5P_s[usI/ph*0pV~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@Gb79+X'F"5Y`EfJOV2A9=!fB]F'tK1LS`,]G+MiTenb&V2-^hqa(5IE#Nr59/!"Qm*5_(BdF!0&h!Yhk/A+\iS'%6tuO$O)9LaZS+flr([1p2&#RS1p/gT[B;rDj-=&=iqUlj(P^/5U@eCFqn4:<lU`l`.HXqG-',hJH.DI.(6L\luSAW`Q'oje[qgVLVIXg%PXe+,<$7('~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GbmK%f(e+0_`ODoa2.):e/i+N3r(.o*Qf\gSNb(bt4FIubi@GIOE=p8Ir3;CbQ@KuG^cdJhODZKQ*upt+*rdZ%!mFmN$*.P)K;`s#]G=8AO3s3DGB.RCOn?[F]bEIg,a>25?B%dh\Z/C6opFE'el@I,P\u\V\]:*JYrrsNJ&d,11VL;$h!43eGu&1X6$+5-h\Vr6!+>4Je,~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001527 00000 n
|
||||||
|
0000001827 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<0d5cf047e754e05f8d574f067785875c><0d5cf047e754e05f8d574f067785875c>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2127
|
||||||
|
%%EOF
|
||||||
106
cucumber/exampleFiles/ghost2.pdf
Normal file
106
cucumber/exampleFiles/ghost2.pdf
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G:CDb.*/<p2MVk["e@)7*Z0@"b%+@f/9pA%_U<oOkVp?PnGRb81iPg?0i?(]%^_CSf##%;<!7Ne/-%RR^p@t7hKYZ9eJVHV]fjjHIB:6DrW+2\p16@*`r^CpQZZH'2Pjqd<.&hM2UO%$Wi$te%4QmS;<E"QS\!deQG_XtuEK>b(UbS>%`/0S`k\\5'TNY0mmgH?`8]i_0~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 207
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]afWJ'Lm;=if<;s>V*7BTJ]oQ@P!(q5S+WG1%>L@?8Ue;c>[fY&&IOd5@t@TY@+q.5T<Z'81"J("KhsBa+&u4"n'#6)AjfImh)%$0tVC:aGk",=aJJH#/4]i.WJr9c"cibYm:M-44<%FFlG0Cl\Z'nmo7C"TR+7dk3T#iD(9Pq'\;rQku%o>A_`50SO&7M04=8M'O<Am~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GYmu@>'Ld5[if35r/JNaJ.A.7fP9RpSN*8k^-sEER0,enq1Rsuo@R/uCO-^&Y`F'9d^a?9)?ns+F&dXm[HMgPn6Ep+%TRk5Nh+!(+[H#H:U^.^(YL,PKS'%j/:3O\hJVEK-UUekJTd[A$N^((K^#0Du`i@,/^f5KiUISGr")3/+f9NF8NO1+iUgm^b"X\cE^+[:s!0]Gu6i~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001524 00000 n
|
||||||
|
0000001822 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<407fc55425168745e56176202aad30c9><407fc55425168745e56176202aad30c9>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2122
|
||||||
|
%%EOF
|
||||||
106
cucumber/exampleFiles/ghost3.pdf
Normal file
106
cucumber/exampleFiles/ghost3.pdf
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>:>hu1e>07BJg5<'#:.C1n)e#(QJ6R1Rsuo_gpn.+0-H5$/#"iYR[B.9\'>7!aDAC*rf/t&6O#aH<?-7IT'\?X(&TcABG=ON*Nq`4k=o&p@3,0*31r<)TAP2Pk94p0\"R-_sY1$AYo[8B\?4R>feLAB\mpjZhp"`@J3;"Fm97#9+W,"eb95\+#p\^HN~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EX'Eriuig+>QHNeD'#n%Sq#n%BW`C'uDUOYK)HdS4E9JMsp+HUmDj&H-t*4?UamXX0peVspk"i_@ba+&u"J>UYDKV_^G,7V==aTZZ<YO7:sNSQ[6"Ja-29NtYjd#=`J@D'h+[QW=:EEb?A<k!f+\`g^?,Vgp7_)91[lR\f.Tkf7VIPLVYM&deF!aYt9Ip^"N",3F'*W~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>J`g!jPCLm;?AgU"fdk"PQZD\d?lRI_oWc[$tp^]O\:3fK8kWeX2&Jcg0+RoJ]j;2j*upu!b4.o&f)b$I@7CfIYjP^#\VjhC=QhQ]^lV-@<0Tam!0.+Dn@("AK%N,Uc7hb+6VoQ$q2q[7]BB92RoY/.j2N028i1jNf'@<1+Fqf$1&"8omHk`#DHP>OT~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001526 00000 n
|
||||||
|
0000001826 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<80da26147a484f2b7573da8151a93d2e><80da26147a484f2b7573da8151a93d2e>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2126
|
||||||
|
%%EOF
|
||||||
1255
cucumber/exampleFiles/images.pdf
Normal file
1255
cucumber/exampleFiles/images.pdf
Normal file
File diff suppressed because it is too large
Load Diff
106
cucumber/exampleFiles/pdfa1.pdf
Normal file
106
cucumber/exampleFiles/pdfa1.pdf
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 206
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G\IO3f&4Lr[@S4&T2aReWZ3N'9",Ncra>5AuK^J(o@r?=EP>b]h[L@XZ8q7#[c:#H2:^/=b,p3^,&f-Q.'H%!U?%N\iVa1pLMlh/41\A8@dF5@0al:-1?L;D%LpL3g\9`.3c6N/Mp=sE/nO%^@%Cc3`]e`qqS@[pkUWemMZC<P\fkqa55u)*hIUoU437-gb!e_*&B/,&~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G\IO3V'LdA_ig"8P1PS=kA5Q_GQ\P]*S3\>Q`jHYt?8UdkV`6]UV*On)+1VMV+A@.iF:*6sWfM9f"s.NmVuMto!p7-+,Rb<.h,pdi-&OQ5KO\RRFj.j"A)ScTQ7$hudF^TnZ'XuQA5"O]rYkt><-DJmj'"Ri>n!4`^m409XX`e)AR'*rGsn6m79.18+^ba=qRuss"-A3k+9~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:.1fBHK`Xl'[i1&AjX(\k8hbgo(QJ6R1Rsuo6_I1A5Gg$JL;D#$J2CX;+Cf*cUHk2%H1XmpWe+qZ5moJ#B]>b%%[d,mfSSkS4A:Q4NlOFfrL7eA,s45"eUSakM;927AA,1"-LZ)&nZ/ah=8_X7:?ZMj@J@;r7d`t]Z0\d39M%:$k8[S5D"2oSap4s80l?~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001523 00000 n
|
||||||
|
0000001823 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<88edee24ee67bd7d6b7cf53cfa2222b0><88edee24ee67bd7d6b7cf53cfa2222b0>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2124
|
||||||
|
%%EOF
|
||||||
106
cucumber/exampleFiles/pdfa2.pdf
Normal file
106
cucumber/exampleFiles/pdfa2.pdf
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
%PDF-1.3
|
||||||
|
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||||
|
1 0 obj
|
||||||
|
<<
|
||||||
|
/F1 2 0 R
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
2 0 obj
|
||||||
|
<<
|
||||||
|
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
3 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 9 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
4 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 10 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
5 0 obj
|
||||||
|
<<
|
||||||
|
/Contents 11 0 R /MediaBox [ 0 0 612 792 ] /Parent 8 0 R /Resources <<
|
||||||
|
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||||
|
>> /Rotate 0 /Trans <<
|
||||||
|
|
||||||
|
>>
|
||||||
|
/Type /Page
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
6 0 obj
|
||||||
|
<<
|
||||||
|
/PageMode /UseNone /Pages 8 0 R /Type /Catalog
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
7 0 obj
|
||||||
|
<<
|
||||||
|
/Author (anonymous) /CreationDate (D:20240718233034+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20240718233034+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||||
|
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
8 0 obj
|
||||||
|
<<
|
||||||
|
/Count 3 /Kids [ 3 0 R 4 0 R 5 0 R ] /Type /Pages
|
||||||
|
>>
|
||||||
|
endobj
|
||||||
|
9 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@GYmu@>'Ld5[if35rI0]sG)F[U^"c>T)"\\os-r:1V0,enq1Rsuo,*67.@k7U.LRF-P.e"CM2V!>iYi<g`nXh!K?n@$t^rY1$+^0'>=B8H6e;F1WmG#,(eS00(Qe9&:O@nI879DTsT,njXAB?`8:>,Hn3*RV!qh4;&@6%]<9Y*>QZ].Z5o;RAZXg7d[#+bphHs_Ep!QR2TZ2~>endstream
|
||||||
|
endobj
|
||||||
|
10 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 210
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G]+0EH(e/_@iZH]:>=,iY1bE)XN?M;1'J/>i&HY;gks]*rj:!DKpb8@`prC#N+9E#o#-<G*!#p7e6j-1sX2k5S,6XmM"taYkfK^k">%usEeEk=sR<UT"dm`rXD;!S`_jS9LU+(R%e'V%WSMfHP.pXZEQqTQq=&D[I[PS(41(NIAZ1R/U?:Z=hSXu!NDF)bpG2F+/I/q/u1-Y~>endstream
|
||||||
|
endobj
|
||||||
|
11 0 obj
|
||||||
|
<<
|
||||||
|
/Filter [ /ASCII85Decode /FlateDecode ] /Length 209
|
||||||
|
>>
|
||||||
|
stream
|
||||||
|
Gap@G_$YcZ'LhbF`EQB$nqi=8S<;#HbK3&f>rnodRPo`Vf4P[3cJidY(I=[K5NWCT'<lHgci?oCRVNST&[k#q4oSC0FWgAt1pD4d_(hIRjn_Nt+cFgJlfm[1U8@/M4r^Pk<@F!@e?%/!-Vq;]nfdLi9]P2M)ck9?)%oNXa_\N<-d"(pjlH%-G`T@Sj&P(j6.@#Xh\Vr6!1iI2/H~>endstream
|
||||||
|
endobj
|
||||||
|
xref
|
||||||
|
0 12
|
||||||
|
0000000000 65535 f
|
||||||
|
0000000073 00000 n
|
||||||
|
0000000104 00000 n
|
||||||
|
0000000211 00000 n
|
||||||
|
0000000404 00000 n
|
||||||
|
0000000598 00000 n
|
||||||
|
0000000792 00000 n
|
||||||
|
0000000860 00000 n
|
||||||
|
0000001156 00000 n
|
||||||
|
0000001227 00000 n
|
||||||
|
0000001526 00000 n
|
||||||
|
0000001827 00000 n
|
||||||
|
trailer
|
||||||
|
<<
|
||||||
|
/ID
|
||||||
|
[<4fcc82a085fe71e34a32d1b23c8b939f><4fcc82a085fe71e34a32d1b23c8b939f>]
|
||||||
|
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||||
|
|
||||||
|
/Info 7 0 R
|
||||||
|
/Root 6 0 R
|
||||||
|
/Size 12
|
||||||
|
>>
|
||||||
|
startxref
|
||||||
|
2127
|
||||||
|
%%EOF
|
||||||
@@ -14,3 +14,8 @@ def after_scenario(context, scenario):
|
|||||||
os.remove('response_file')
|
os.remove('response_file')
|
||||||
if hasattr(context, 'file_name') and os.path.exists(context.file_name):
|
if hasattr(context, 'file_name') and os.path.exists(context.file_name):
|
||||||
os.remove(context.file_name)
|
os.remove(context.file_name)
|
||||||
|
|
||||||
|
# Remove any temporary files
|
||||||
|
for temp_file in os.listdir('.'):
|
||||||
|
if temp_file.startswith('genericNonCustomisableName') or temp_file.startswith('temp_image_'):
|
||||||
|
os.remove(temp_file)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@example
|
@example @general
|
||||||
Feature: API Validation
|
Feature: API Validation
|
||||||
|
|
||||||
@positive @password
|
@positive @password
|
||||||
@@ -92,10 +92,10 @@ Feature: API Validation
|
|||||||
| threshold | 90 |
|
| threshold | 90 |
|
||||||
| whitePercent | 99.9 |
|
| whitePercent | 99.9 |
|
||||||
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
||||||
Then the response content type should be "application/pdf"
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response file should have extension ".zip"
|
||||||
|
And the response ZIP should contain 1 files
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
And the response PDF should contain 0 pages
|
|
||||||
And the response status code should be 200
|
|
||||||
|
|
||||||
@positive @flatten
|
@positive @flatten
|
||||||
Scenario: Flatten PDF
|
Scenario: Flatten PDF
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Feature: API Validation
|
|||||||
@ocr @positive
|
@ocr @positive
|
||||||
Scenario: Extract Image Scans
|
Scenario: Extract Image Scans
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I generate a PDF file as "fileInput"
|
||||||
And the pdf contains 3 images on 2 pages
|
And the pdf contains 3 images of size 300x300 on 2 pages
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| angleThreshold | 5 |
|
| angleThreshold | 5 |
|
||||||
@@ -125,8 +125,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
@ocr
|
@ocr
|
||||||
Scenario: PDFA
|
Scenario: PDFA
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| outputFormat | pdfa |
|
| outputFormat | pdfa |
|
||||||
@@ -137,8 +136,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
@ocr
|
@ocr
|
||||||
Scenario: PDFA1
|
Scenario: PDFA1
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| outputFormat | pdfa-1 |
|
| outputFormat | pdfa-1 |
|
||||||
@@ -149,8 +147,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 4 |
|
| optimizeLevel | 4 |
|
||||||
@@ -161,8 +158,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 1 |
|
| optimizeLevel | 1 |
|
||||||
@@ -175,8 +171,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @ghostscript @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
||||||
And the pdf contains 3 pages with random text
|
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
| optimizeLevel | 1 |
|
| optimizeLevel | 1 |
|
||||||
|
|||||||
@@ -94,3 +94,23 @@ Feature: API Validation
|
|||||||
| 1 | 10 | 2 | 10 |
|
| 1 | 10 | 2 | 10 |
|
||||||
|
|
||||||
|
|
||||||
|
@extract-images
|
||||||
|
Scenario Outline: Extract Image Scans
|
||||||
|
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
|
||||||
|
And the request data includes
|
||||||
|
| parameter | value |
|
||||||
|
| format | <format> |
|
||||||
|
When I send the API request to the endpoint "/api/v1/misc/extract-images"
|
||||||
|
Then the response content type should be "application/octet-stream"
|
||||||
|
And the response file should have extension ".zip"
|
||||||
|
And the response ZIP should contain 20 files
|
||||||
|
And the response file should have size greater than 0
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
| format |
|
||||||
|
| png |
|
||||||
|
| gif |
|
||||||
|
| jpeg |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import io
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from reportlab.lib.pagesizes import letter
|
from reportlab.lib.pagesizes import letter
|
||||||
|
from reportlab.lib.utils import ImageReader
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import requests
|
import requests
|
||||||
import zipfile
|
import zipfile
|
||||||
import shutil
|
import shutil
|
||||||
|
import re
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# GIVEN #
|
# GIVEN #
|
||||||
@@ -43,8 +46,6 @@ def step_use_example_file(context, filePath, fileInput):
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
raise FileNotFoundError(f"The example file '{filePath}' does not exist.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@given('the pdf contains {page_count:d} pages')
|
@given('the pdf contains {page_count:d} pages')
|
||||||
def step_pdf_contains_pages(context, page_count):
|
def step_pdf_contains_pages(context, page_count):
|
||||||
writer = PdfWriter()
|
writer = PdfWriter()
|
||||||
@@ -66,8 +67,6 @@ def step_pdf_contains_blank_pages(context, page_count):
|
|||||||
context.files[context.param_name].close()
|
context.files[context.param_name].close()
|
||||||
context.files[context.param_name] = open(context.file_name, 'rb')
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_black_box_image(file_name, size):
|
def create_black_box_image(file_name, size):
|
||||||
can = canvas.Canvas(file_name, pagesize=size)
|
can = canvas.Canvas(file_name, pagesize=size)
|
||||||
width, height = size
|
width, height = size
|
||||||
@@ -76,9 +75,25 @@ def create_black_box_image(file_name, size):
|
|||||||
can.showPage()
|
can.showPage()
|
||||||
can.save()
|
can.save()
|
||||||
|
|
||||||
def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
@given(u'the pdf contains {image_count:d} images of size {width:d}x{height:d} on {page_count:d} pages')
|
||||||
page_width, page_height = letter
|
def step_impl(context, image_count, width, height, page_count):
|
||||||
box_size = 72 # 1 inch by 1 inch black box
|
context.param_name = "fileInput"
|
||||||
|
context.file_name = "genericNonCustomisableName.pdf"
|
||||||
|
create_pdf_with_images_and_boxes(context.file_name, image_count, page_count, width, height)
|
||||||
|
if not hasattr(context, 'files'):
|
||||||
|
context.files = {}
|
||||||
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
def add_black_boxes_to_image(image):
|
||||||
|
if isinstance(image, str):
|
||||||
|
image = Image.open(image)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
draw.rectangle([(0, 0), image.size], fill=(0, 0, 0)) # Fill image with black
|
||||||
|
return image
|
||||||
|
|
||||||
|
def create_pdf_with_images_and_boxes(file_name, image_count, page_count, image_width, image_height):
|
||||||
|
page_width, page_height = max(letter[0], image_width), max(letter[1], image_height)
|
||||||
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
|
boxes_per_page = image_count // page_count + (1 if image_count % page_count != 0 else 0)
|
||||||
|
|
||||||
writer = PdfWriter()
|
writer = PdfWriter()
|
||||||
@@ -86,15 +101,31 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
|||||||
|
|
||||||
for page in range(page_count):
|
for page in range(page_count):
|
||||||
packet = io.BytesIO()
|
packet = io.BytesIO()
|
||||||
can = canvas.Canvas(packet, pagesize=letter)
|
can = canvas.Canvas(packet, pagesize=(page_width, page_height))
|
||||||
|
|
||||||
for i in range(boxes_per_page):
|
for i in range(boxes_per_page):
|
||||||
if box_counter >= image_count:
|
if box_counter >= image_count:
|
||||||
break
|
break
|
||||||
x = (i % (page_width // box_size)) * box_size
|
|
||||||
y = page_height - ((i // (page_width // box_size) + 1) * box_size)
|
# Simulating a dynamic image creation (replace this with your actual image creation logic)
|
||||||
can.setFillColorRGB(0, 0, 0)
|
# For demonstration, we'll create a simple black image
|
||||||
can.rect(x, y, box_size, box_size, fill=1)
|
dummy_image = Image.new('RGB', (image_width, image_height), color='white') # Create a white image
|
||||||
|
dummy_image = add_black_boxes_to_image(dummy_image) # Add black boxes
|
||||||
|
|
||||||
|
# Convert the PIL Image to bytes to pass to drawImage
|
||||||
|
image_bytes = io.BytesIO()
|
||||||
|
dummy_image.save(image_bytes, format='PNG')
|
||||||
|
image_bytes.seek(0)
|
||||||
|
|
||||||
|
# Check if the image fits in the current page dimensions
|
||||||
|
x = (i % (page_width // image_width)) * image_width
|
||||||
|
y = page_height - (((i % (page_height // image_height)) + 1) * image_height)
|
||||||
|
|
||||||
|
if x + image_width > page_width or y < 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add the image to the PDF
|
||||||
|
can.drawImage(ImageReader(image_bytes), x, y, width=image_width, height=image_height)
|
||||||
box_counter += 1
|
box_counter += 1
|
||||||
|
|
||||||
can.showPage()
|
can.showPage()
|
||||||
@@ -103,9 +134,16 @@ def create_pdf_with_black_boxes(file_name, image_count, page_count):
|
|||||||
new_pdf = PdfReader(packet)
|
new_pdf = PdfReader(packet)
|
||||||
writer.add_page(new_pdf.pages[0])
|
writer.add_page(new_pdf.pages[0])
|
||||||
|
|
||||||
|
# Write the PDF to file
|
||||||
with open(file_name, 'wb') as f:
|
with open(file_name, 'wb') as f:
|
||||||
writer.write(f)
|
writer.write(f)
|
||||||
|
|
||||||
|
# Clean up temporary image files
|
||||||
|
for i in range(image_count):
|
||||||
|
temp_image_path = f"temp_image_{i}.png"
|
||||||
|
if os.path.exists(temp_image_path):
|
||||||
|
os.remove(temp_image_path)
|
||||||
|
|
||||||
@given('the pdf contains {image_count:d} images on {page_count:d} pages')
|
@given('the pdf contains {image_count:d} images on {page_count:d} pages')
|
||||||
def step_pdf_contains_images(context, image_count, page_count):
|
def step_pdf_contains_images(context, image_count, page_count):
|
||||||
if not hasattr(context, 'param_name'):
|
if not hasattr(context, 'param_name'):
|
||||||
@@ -118,7 +156,6 @@ def step_pdf_contains_images(context, image_count, page_count):
|
|||||||
context.files[context.param_name].close()
|
context.files[context.param_name].close()
|
||||||
context.files[context.param_name] = open(context.file_name, 'rb')
|
context.files[context.param_name] = open(context.file_name, 'rb')
|
||||||
|
|
||||||
|
|
||||||
@given('the pdf contains {page_count:d} pages with random text')
|
@given('the pdf contains {page_count:d} pages with random text')
|
||||||
def step_pdf_contains_pages_with_random_text(context, page_count):
|
def step_pdf_contains_pages_with_random_text(context, page_count):
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
@@ -186,6 +223,21 @@ def save_generated_pdf(context, filename):
|
|||||||
# WHEN #
|
# WHEN #
|
||||||
########
|
########
|
||||||
|
|
||||||
|
@when('I send a GET request to "{endpoint}"')
|
||||||
|
def step_send_get_request(context, endpoint):
|
||||||
|
base_url = "http://localhost:8080"
|
||||||
|
full_url = f"{base_url}{endpoint}"
|
||||||
|
response = requests.get(full_url)
|
||||||
|
context.response = response
|
||||||
|
|
||||||
|
@when('I send a GET request to "{endpoint}" with parameters')
|
||||||
|
def step_send_get_request_with_params(context, endpoint):
|
||||||
|
base_url = "http://localhost:8080"
|
||||||
|
params = {row['parameter']: row['value'] for row in context.table}
|
||||||
|
full_url = f"{base_url}{endpoint}"
|
||||||
|
response = requests.get(full_url, params=params)
|
||||||
|
context.response = response
|
||||||
|
|
||||||
@when('I send the API request to the endpoint "{endpoint}"')
|
@when('I send the API request to the endpoint "{endpoint}"')
|
||||||
def step_send_api_request(context, endpoint):
|
def step_send_api_request(context, endpoint):
|
||||||
url = f"http://localhost:8080{endpoint}"
|
url = f"http://localhost:8080{endpoint}"
|
||||||
@@ -278,7 +330,6 @@ def step_save_response_file(context, filename):
|
|||||||
f.write(context.response.content)
|
f.write(context.response.content)
|
||||||
print(f"Saved response content to {filename}")
|
print(f"Saved response content to {filename}")
|
||||||
|
|
||||||
|
|
||||||
@then('the response PDF should contain {page_count:d} pages')
|
@then('the response PDF should contain {page_count:d} pages')
|
||||||
def step_check_response_pdf_page_count(context, page_count):
|
def step_check_response_pdf_page_count(context, page_count):
|
||||||
response_file = io.BytesIO(context.response.content)
|
response_file = io.BytesIO(context.response.content)
|
||||||
@@ -305,3 +356,26 @@ def step_check_response_zip_doc_page_count(context, doc_count, pages_per_doc):
|
|||||||
reader = PdfReader(pdf_file)
|
reader = PdfReader(pdf_file)
|
||||||
actual_pages_per_doc = len(reader.pages)
|
actual_pages_per_doc = len(reader.pages)
|
||||||
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
|
assert actual_pages_per_doc == pages_per_doc, f"Expected {pages_per_doc} pages per document but got {actual_pages_per_doc} pages in document {file_name}"
|
||||||
|
|
||||||
|
@then('the JSON value of "{key}" should be "{expected_value}"')
|
||||||
|
def step_check_json_value(context, key, expected_value):
|
||||||
|
actual_value = context.response.json().get(key)
|
||||||
|
assert actual_value == expected_value, \
|
||||||
|
f"Expected JSON value for '{key}' to be '{expected_value}' but got '{actual_value}'"
|
||||||
|
|
||||||
|
@then('JSON list entry containing "{identifier_key}" as "{identifier_value}" should have "{target_key}" as "{target_value}"')
|
||||||
|
def step_check_json_list_entry(context, identifier_key, identifier_self, target_key, target_value):
|
||||||
|
json_response = context.response.json()
|
||||||
|
for entry in json_response:
|
||||||
|
if entry.get(identifier_key) == identifier_value:
|
||||||
|
assert entry.get(target_key) == target_value, \
|
||||||
|
f"Expected {target_key} to be {target_value} in entry where {identifier_key} is {identifier_value}, but found {entry.get(target_key)}"
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"No entry with {identifier_key} as {identifier_value} found")
|
||||||
|
|
||||||
|
@then('the response should match the regex "{pattern}"')
|
||||||
|
def step_response_matches_regex(context, pattern):
|
||||||
|
response_text = context.response.text
|
||||||
|
assert re.match(pattern, response_text), \
|
||||||
|
f"Response '{response_text}' does not match the expected pattern '{pattern}'"
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ services:
|
|||||||
DOCKER_ENABLE_SECURITY: "false"
|
DOCKER_ENABLE_SECURITY: "false"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
|
||||||
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: Stirling-PDF
|
UI_APPNAME: Stirling-PDF
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ ignore = [
|
|||||||
'text',
|
'text',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[da_DK]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[de_DE]
|
[de_DE]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
@@ -87,6 +92,11 @@ ignore = [
|
|||||||
'watermark.type.2',
|
'watermark.type.2',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[ga_IE]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[hi_IN]
|
[hi_IN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@@ -171,7 +181,9 @@ ignore = [
|
|||||||
|
|
||||||
[pt_BR]
|
[pt_BR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'changeMetadata.trapped',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
]
|
]
|
||||||
|
|
||||||
[pt_PT]
|
[pt_PT]
|
||||||
@@ -212,6 +224,14 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[th_TH]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
[tr_TR]
|
[tr_TR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@@ -222,6 +242,14 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[vi_VN]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'showJS.tags',
|
||||||
|
]
|
||||||
|
|
||||||
[zh_CN]
|
[zh_CN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ fi
|
|||||||
umask "$UMASK" || true
|
umask "$UMASK" || true
|
||||||
|
|
||||||
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
|
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
|
||||||
apk add --no-cache calibre@testing
|
echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
|
||||||
|
#apk add --no-cache calibre@testing
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$FAT_DOCKER" != "true" ]]; then
|
if [[ "$FAT_DOCKER" != "true" ]]; then
|
||||||
|
|||||||
174
scripts/png_to_webp.py
Normal file
174
scripts/png_to_webp.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Author: Ludy87
|
||||||
|
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
To convert a PDF file to WebP images with each page as a separate WebP file:
|
||||||
|
python script.py input.pdf output_directory
|
||||||
|
|
||||||
|
To convert a PDF file to a single WebP image:
|
||||||
|
python script.py input.pdf output_directory --single
|
||||||
|
|
||||||
|
To adjust the DPI resolution for rendering PDF pages:
|
||||||
|
python script.py input.pdf output_directory --dpi 150
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from pdf2image import convert_from_path
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
|
||||||
|
"""
|
||||||
|
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_image_path : str
|
||||||
|
Path to the input image file.
|
||||||
|
output_image_path : str
|
||||||
|
Path where the output WebP image will be saved.
|
||||||
|
max_size : tuple of int, optional
|
||||||
|
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Open the image
|
||||||
|
image = Image.open(input_image_path)
|
||||||
|
width, height = image.size
|
||||||
|
max_width, max_height = max_size
|
||||||
|
|
||||||
|
# Check if the image dimensions exceed the maximum allowed dimensions
|
||||||
|
if width > max_width or height > max_height:
|
||||||
|
# Calculate the scaling ratio
|
||||||
|
ratio = min(max_width / width, max_height / height)
|
||||||
|
new_width = int(width * ratio)
|
||||||
|
new_height = int(height * ratio)
|
||||||
|
|
||||||
|
# Resize the image
|
||||||
|
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
resized_image.save(output_image_path, format="WEBP", quality=100)
|
||||||
|
print(
|
||||||
|
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# If dimensions are within the allowed limits, save the image directly
|
||||||
|
image.save(output_image_path, format="WEBP", quality=100)
|
||||||
|
print(f"The image was successfully saved as WebP: {output_image_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An error occurred: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def convert_image_to_webp(input_image, output_file):
|
||||||
|
"""
|
||||||
|
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input_image : str
|
||||||
|
Path to the input image file.
|
||||||
|
output_file : str
|
||||||
|
Path where the output WebP image will be saved.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# Resize the image if it exceeds the maximum dimensions
|
||||||
|
resize_image(input_image, output_file, max_size=(16383, 16383))
|
||||||
|
|
||||||
|
|
||||||
|
def pdf_to_webp(pdf_path, output_dir, dpi=300):
|
||||||
|
"""
|
||||||
|
Convert each page of a PDF file to WebP images.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pdf_path : str
|
||||||
|
Path to the input PDF file.
|
||||||
|
output_dir : str
|
||||||
|
Directory where the WebP images will be saved.
|
||||||
|
dpi : int, optional
|
||||||
|
DPI resolution for rendering PDF pages. Default is 300.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
# Convert the PDF to a list of images
|
||||||
|
images = convert_from_path(pdf_path, dpi=dpi)
|
||||||
|
|
||||||
|
for page_number, image in enumerate(images):
|
||||||
|
# Define temporary PNG path
|
||||||
|
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
|
||||||
|
image.save(temp_png_path, format="PNG")
|
||||||
|
|
||||||
|
# Define the output path for WebP
|
||||||
|
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
|
||||||
|
|
||||||
|
# Convert PNG to WebP
|
||||||
|
convert_image_to_webp(temp_png_path, output_path)
|
||||||
|
|
||||||
|
# Delete the temporary PNG file
|
||||||
|
os.remove(temp_png_path)
|
||||||
|
|
||||||
|
|
||||||
|
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
|
||||||
|
"""
|
||||||
|
Main function to handle conversion from PDF to WebP images.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pdf_image_path : str
|
||||||
|
Path to the input PDF file or image.
|
||||||
|
output_dir : str
|
||||||
|
Directory where the WebP images will be saved.
|
||||||
|
dpi : int, optional
|
||||||
|
DPI resolution for rendering PDF pages. Default is 300.
|
||||||
|
single_images_flag : bool, optional
|
||||||
|
If True, combine all pages into a single WebP image. Default is False.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
if single_images_flag:
|
||||||
|
# Combine all pages into a single WebP image
|
||||||
|
output_path = os.path.join(output_dir, "combined_image.webp")
|
||||||
|
convert_image_to_webp(pdf_image_path, output_path)
|
||||||
|
else:
|
||||||
|
# Convert each PDF page to a separate WebP image
|
||||||
|
pdf_to_webp(pdf_image_path, output_dir, dpi)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
|
||||||
|
parser.add_argument("pdf_path", help="The path to the input PDF file.")
|
||||||
|
parser.add_argument(
|
||||||
|
"output_dir", help="The directory where the WebP images should be saved."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dpi",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="The DPI resolution for rendering the PDF pages (default: 300).",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--single",
|
||||||
|
action="store_true",
|
||||||
|
help="Combine all pages into a single WebP image.",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
os.makedirs(args.output_dir, exist_ok=True)
|
||||||
|
main(
|
||||||
|
args.pdf_path,
|
||||||
|
args.output_dir,
|
||||||
|
dpi=args.dpi,
|
||||||
|
single_images_flag=args.single,
|
||||||
|
)
|
||||||
@@ -45,7 +45,6 @@ public class SPdfApplication {
|
|||||||
// Check if the BROWSER_OPEN environment variable is set to true
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||||
|
|
||||||
if (browserOpen) {
|
if (browserOpen) {
|
||||||
try {
|
try {
|
||||||
String url = "http://localhost:" + getNonStaticPort();
|
String url = "http://localhost:" + getNonStaticPort();
|
||||||
@@ -79,13 +78,14 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
// custom javs settings file
|
// custom javs settings file
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
String existingLocation =
|
||||||
if (!existing.isEmpty()) {
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
existing += ",";
|
if (!existingLocation.isEmpty()) {
|
||||||
|
existingLocation += ",";
|
||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existing + "file:configs/custom_settings.yml");
|
existingLocation + "file:configs/custom_settings.yml");
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,25 +32,25 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Map<String, String> parameters = new HashMap<>();
|
Map<String, String> allowedParameters = new HashMap<>();
|
||||||
|
|
||||||
// Keep only the allowed parameters
|
// Keep only the allowed parameters
|
||||||
String[] queryParameters = queryString.split("&");
|
String[] queryParameters = queryString.split("&");
|
||||||
for (String param : queryParameters) {
|
for (String param : queryParameters) {
|
||||||
String[] keyValue = param.split("=");
|
String[] keyValuePair = param.split("=");
|
||||||
if (keyValue.length != 2) {
|
if (keyValuePair.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
if (ALLOWED_PARAMS.contains(keyValuePair[0])) {
|
||||||
parameters.put(keyValue[0], keyValue[1]);
|
allowedParameters.put(keyValuePair[0], keyValuePair[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any parameters that are not allowed
|
// If there are any parameters that are not allowed
|
||||||
if (parameters.size() != queryParameters.length) {
|
if (allowedParameters.size() != queryParameters.length) {
|
||||||
// Construct new query string
|
// Construct new query string
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
|
||||||
if (newQueryString.length() > 0) {
|
if (newQueryString.length() > 0) {
|
||||||
newQueryString.append("&");
|
newQueryString.append("&");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.simpleyaml.configuration.comments.CommentType;
|
import org.simpleyaml.configuration.comments.CommentType;
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
|
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||||
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
@@ -71,9 +73,17 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
||||||
|
DumperOptions yamlOptionsSettingsTemplateFile =
|
||||||
|
((SimpleYamlImplementation) settingsTemplateFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsSettingsTemplateFile.setSplitLines(false);
|
||||||
settingsTemplateFile.loadWithComments();
|
settingsTemplateFile.loadWithComments();
|
||||||
|
|
||||||
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
||||||
|
DumperOptions yamlOptionsSettingsFile =
|
||||||
|
((SimpleYamlImplementation) settingsFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsSettingsFile.setSplitLines(false);
|
||||||
settingsFile.loadWithComments();
|
settingsFile.loadWithComments();
|
||||||
|
|
||||||
// Load headers and comments
|
// Load headers and comments
|
||||||
@@ -81,6 +91,10 @@ public class ConfigInitializer
|
|||||||
|
|
||||||
// Create a new file for temporary settings
|
// Create a new file for temporary settings
|
||||||
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
||||||
|
DumperOptions yamlOptionsTempSettingFile =
|
||||||
|
((SimpleYamlImplementation) tempSettingFile.getImplementation())
|
||||||
|
.getDumperOptions();
|
||||||
|
yamlOptionsTempSettingFile.setSplitLines(false);
|
||||||
tempSettingFile.createNewFile(true);
|
tempSettingFile.createNewFile(true);
|
||||||
tempSettingFile.setHeader(header);
|
tempSettingFile.setHeader(header);
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", "auto-rename");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
|
addEndpointToGroup("Other", "remove-image-pdf");
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
@@ -165,6 +166,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Python", REMOVE_BLANKS);
|
addEndpointToGroup("Python", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
|
addEndpointToGroup("Python", "pdf-to-img");
|
||||||
|
|
||||||
// openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
@@ -221,6 +223,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Java", "pdf-to-text");
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
|
addEndpointToGroup("Java", "remove-image-pdf");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
@@ -15,17 +14,16 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
|
||||||
|
|
||||||
public CustomAuthenticationFailureHandler(
|
public CustomAuthenticationFailureHandler(
|
||||||
final LoginAttemptService loginAttemptService, UserService userService) {
|
final LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
@@ -39,14 +37,17 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (exception instanceof DisabledException) {
|
||||||
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
logger.error("Failed login attempt from IP: {}", ip);
|
log.error("Failed login attempt from IP: {}", ip);
|
||||||
|
|
||||||
String contextPath = request.getContextPath();
|
if (exception instanceof LockedException) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
||||||
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
|
||||||
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
|
||||||
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,20 +55,25 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
||||||
logger.info(
|
log.info(
|
||||||
"Remaining attempts for user {}: {}",
|
"Remaining attempts for user {}: {}",
|
||||||
optUser.get().getUsername(),
|
username,
|
||||||
loginAttemptService.getRemainingAttempts(username));
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
loginAttemptService.loginFailed(username);
|
loginAttemptService.loginFailed(username);
|
||||||
if (loginAttemptService.isBlocked(username)
|
if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
|
||||||
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
||||||
response.sendRedirect(contextPath + "/login?error=locked");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
if (exception instanceof BadCredentialsException
|
||||||
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
|| exception instanceof UsernameNotFoundException) {
|
||||||
response.sendRedirect(contextPath + "/login?error=badcredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof InternalAuthenticationServiceException
|
||||||
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
|
public CustomAuthenticationSuccessHandler(
|
||||||
|
LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -27,6 +32,10 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
String userName = request.getParameter("username");
|
String userName = request.getParameter("username");
|
||||||
|
if (userService.isUserDisabled(userName)) {
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
loginAttemptService.loginSucceeded(userName);
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
|
|||||||
@@ -2,32 +2,26 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
@Autowired SessionRegistry sessionRegistry;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
if (session != null) {
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
String sessionId = session.getId();
|
getRedirectStrategy()
|
||||||
sessionRegistry.removeSessionInformation(sessionId);
|
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
|
||||||
session.invalidate();
|
return;
|
||||||
logger.debug("Session invalidated: " + sessionId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import java.nio.file.Paths;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
|
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||||
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -92,6 +94,9 @@ public class InitialSecuritySetup {
|
|||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
||||||
|
yamlOptionssettingsYml.setSplitLines(false);
|
||||||
|
|
||||||
settingsYml.loadWithComments();
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -17,8 +15,6 @@ public class LoginAttemptService {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
|
|
||||||
|
|
||||||
private int MAX_ATTEMPT;
|
private int MAX_ATTEMPT;
|
||||||
private long ATTEMPT_INCREMENT_TIME;
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ import org.springframework.security.config.http.SessionCreationPolicy;
|
|||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
@@ -37,6 +35,7 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF
|
|||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
@@ -47,7 +46,7 @@ import stirling.software.SPDF.model.provider.KeycloakProvider;
|
|||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity()
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@@ -73,11 +72,7 @@ public class SecurityConfiguration {
|
|||||||
@Autowired private LoginAttemptService loginAttemptService;
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
|
@Autowired private SessionPersistentRegistry sessionRegistry;
|
||||||
@Bean
|
|
||||||
public SessionRegistry sessionRegistry() {
|
|
||||||
return new SessionRegistryImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
@@ -94,7 +89,7 @@ public class SecurityConfiguration {
|
|||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
.maximumSessions(10)
|
.maximumSessions(10)
|
||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry())
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
@@ -103,7 +98,7 @@ public class SecurityConfiguration {
|
|||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler(
|
new CustomAuthenticationSuccessHandler(
|
||||||
loginAttemptService))
|
loginAttemptService, userService))
|
||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
@@ -160,7 +155,11 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().getOAUTH2() != null
|
if (applicationProperties.getSecurity().getOAUTH2() != null
|
||||||
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()
|
||||||
|
&& !applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getLoginMethod()
|
||||||
|
.equalsIgnoreCase("normal")) {
|
||||||
|
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
@@ -192,9 +191,7 @@ public class SecurityConfiguration {
|
|||||||
logout ->
|
logout ->
|
||||||
logout.logoutSuccessHandler(
|
logout.logoutSuccessHandler(
|
||||||
new CustomOAuth2LogoutSuccessHandler(
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
this.applicationProperties,
|
applicationProperties)));
|
||||||
sessionRegistry()))
|
|
||||||
.invalidateHttpSession(true));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -8,9 +11,11 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
@@ -18,15 +23,17 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired private UserDetailsService userDetailsService;
|
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
@@ -51,15 +58,20 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
try {
|
try {
|
||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
// provider for API keys.
|
// provider for API keys.
|
||||||
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
Optional<User> user = userService.loadUserByApiKey(apiKey);
|
||||||
if (userDetails == null) {
|
if (!user.isPresent()) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
authentication =
|
List<SimpleGrantedAuthority> authorities =
|
||||||
new ApiKeyAuthenticationToken(
|
user.get().getAuthorities().stream()
|
||||||
userDetails, apiKey, userDetails.getAuthorities());
|
.map(
|
||||||
|
authority ->
|
||||||
|
new SimpleGrantedAuthority(
|
||||||
|
authority.getAuthority()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
// If API key authentication fails, deny the request
|
// If API key authentication fails, deny the request
|
||||||
@@ -87,6 +99,43 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the authenticated user is disabled and invalidate their session if so
|
||||||
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username = null;
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
username = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
username = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
username = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
|
||||||
|
if (username != null) {
|
||||||
|
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
||||||
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
|
if (!isUserExists || isUserDisabled) {
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionsInformation.expireNow();
|
||||||
|
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUserExists) {
|
||||||
|
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isUserDisabled) {
|
||||||
|
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,16 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
@@ -40,6 +44,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired private MessageSource messageSource;
|
@Autowired private MessageSource messageSource;
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
@@ -48,7 +54,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -60,8 +66,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
User user = getUserByApiKey(apiKey);
|
Optional<User> user = getUserByApiKey(apiKey);
|
||||||
if (user == null) {
|
if (!user.isPresent()) {
|
||||||
throw new UsernameNotFoundException("API key is not valid");
|
throw new UsernameNotFoundException("API key is not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +75,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
return new UsernamePasswordAuthenticationToken(
|
return new UsernamePasswordAuthenticationToken(
|
||||||
user, // principal (typically the user)
|
user, // principal (typically the user)
|
||||||
null, // credentials (we don't expose the password or API key here)
|
null, // credentials (we don't expose the password or API key here)
|
||||||
getAuthorities(user) // user's authorities (roles/permissions)
|
getAuthorities(user.get()) // user's authorities (roles/permissions)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +90,17 @@ public class UserService implements UserServiceInterface {
|
|||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
apiKey = UUID.randomUUID().toString();
|
apiKey = UUID.randomUUID().toString();
|
||||||
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
} while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
User user =
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
userRepository
|
if (user.isPresent()) {
|
||||||
.findByUsernameIgnoreCase(username)
|
user.get().setApiKey(generateApiKey());
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
return userRepository.save(user.get());
|
||||||
|
}
|
||||||
user.setApiKey(generateApiKey());
|
throw new UsernameNotFoundException("User not found");
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
@@ -104,39 +109,40 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
userRepository
|
findByUsernameIgnoreCase(username)
|
||||||
.findByUsernameIgnoreCase(username)
|
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValidApiKey(String apiKey) {
|
public boolean isValidApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey) != null;
|
return userRepository.findByApiKey(apiKey).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public User getUserByApiKey(String apiKey) {
|
public Optional<User> getUserByApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserDetails loadUserByApiKey(String apiKey) {
|
public Optional<User> loadUserByApiKey(String apiKey) {
|
||||||
User user = userRepository.findByApiKey(apiKey);
|
Optional<User> user = userRepository.findByApiKey(apiKey);
|
||||||
if (user != null) {
|
|
||||||
// Convert your User entity to a UserDetails object with authorities
|
if (user.isPresent()) {
|
||||||
return new org.springframework.security.core.userdetails.User(
|
return user;
|
||||||
user.getUsername(),
|
|
||||||
user.getPassword(), // you might not need this for API key auth
|
|
||||||
getAuthorities(user));
|
|
||||||
}
|
}
|
||||||
return null; // or throw an exception
|
return null; // or throw an exception
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType)
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||||
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
@@ -144,7 +150,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setFirstLogin(false);
|
user.setFirstLogin(false);
|
||||||
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setAuthenticationType(authenticationType);
|
user.setAuthenticationType(authenticationType);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
@@ -186,7 +192,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUser(String username) {
|
public void deleteUser(String username) {
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
for (Authority authority : userOpt.get().getAuthorities()) {
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
@@ -195,21 +201,20 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
userRepository.delete(userOpt.get());
|
userRepository.delete(userOpt.get());
|
||||||
}
|
}
|
||||||
|
invalidateUserSessions(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExists(String username) {
|
public boolean usernameExists(String username) {
|
||||||
return userRepository.findByUsername(username).isPresent();
|
return findByUsername(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExistsIgnoreCase(String username) {
|
public boolean usernameExistsIgnoreCase(String username) {
|
||||||
return userRepository.findByUsernameIgnoreCase(username).isPresent();
|
return findByUsernameIgnoreCase(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasUsers() {
|
public boolean hasUsers() {
|
||||||
long userCount = userRepository.count();
|
long userCount = userRepository.count();
|
||||||
if (userRepository
|
if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
|
||||||
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
|
|
||||||
.isPresent()) {
|
|
||||||
userCount -= 1;
|
userCount -= 1;
|
||||||
}
|
}
|
||||||
return userCount > 0;
|
return userCount > 0;
|
||||||
@@ -217,7 +222,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public void updateUserSettings(String username, Map<String, String> updates)
|
public void updateUserSettings(String username, Map<String, String> updates)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
Map<String, String> settingsMap = user.getSettings();
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
@@ -268,10 +273,17 @@ public class UserService implements UserServiceInterface {
|
|||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeRole(User user, String newRole) {
|
public void changeRole(User user, String newRole) throws IOException {
|
||||||
Authority userAuthority = this.findRole(user);
|
Authority userAuthority = this.findRole(user);
|
||||||
userAuthority.setAuthority(newRole);
|
userAuthority.setAuthority(newRole);
|
||||||
authorityRepository.save(userAuthority);
|
authorityRepository.save(userAuthority);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
|
||||||
|
user.setEnabled(enbeled);
|
||||||
|
userRepository.save(user);
|
||||||
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
@@ -295,14 +307,40 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasPassword(String username) {
|
public boolean hasPassword(String username) {
|
||||||
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent() && user.get().hasPassword();
|
return user.isPresent() && user.get().hasPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAuthenticationTypeByUsername(
|
public boolean isAuthenticationTypeByUsername(
|
||||||
String username, AuthenticationType authenticationType) {
|
String username, AuthenticationType authenticationType) {
|
||||||
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent()
|
return user.isPresent()
|
||||||
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUserDisabled(String username) {
|
||||||
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
|
return userOpt.map(user -> !user.isEnabled()).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidateUserSessions(String username) {
|
||||||
|
String usernameP = "";
|
||||||
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
|
for (SessionInformation sessionsInformation :
|
||||||
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
usernameP = userDetails.getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||||
|
usernameP = oAuth2User.getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
usernameP = (String) principal;
|
||||||
|
}
|
||||||
|
if (usernameP.equalsIgnoreCase(username)) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.nio.file.Paths;
|
|||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
@@ -131,11 +132,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||||
Path insertOutputFilePath =
|
Path insertOutputFilePath =
|
||||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||||
String query = "SCRIPT SIMPLE COLUMNS DROP to '" + insertOutputFilePath.toString() + "';";
|
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||||
Statement stmt = conn.createStatement()) {
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
stmt.execute(query);
|
stmt.setString(1, insertOutputFilePath.toString());
|
||||||
|
stmt.execute();
|
||||||
log.info("Database export completed: {}", insertOutputFilePath);
|
log.info("Database export completed: {}", insertOutputFilePath);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.error("Error during database export: {}", e.getMessage(), e);
|
log.error("Error during database export: {}", e.getMessage(), e);
|
||||||
@@ -177,11 +179,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean executeDatabaseScript(Path scriptPath) {
|
private boolean executeDatabaseScript(Path scriptPath) {
|
||||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
String query = "RUNSCRIPT from ?;";
|
||||||
Statement stmt = conn.createStatement()) {
|
|
||||||
|
|
||||||
String query = "RUNSCRIPT from '" + scriptPath.toString() + "';";
|
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||||
stmt.execute(query);
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
stmt.setString(1, scriptPath.toString());
|
||||||
|
stmt.execute();
|
||||||
log.info("Database import completed: {}", scriptPath);
|
log.info("Database import completed: {}", scriptPath);
|
||||||
return true;
|
return true;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.springframework.security.authentication.DisabledException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
@@ -13,19 +13,34 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomOAuth2AuthenticationFailureHandler
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
extends SimpleUrlAuthenticationFailureHandler {
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (exception instanceof BadCredentialsException) {
|
||||||
|
log.error("BadCredentialsException", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof DisabledException) {
|
||||||
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof LockedException) {
|
||||||
|
log.error("Account locked: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (exception instanceof OAuth2AuthenticationException) {
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
@@ -34,17 +49,13 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
if (error.getErrorCode().equals("Password must not be null")) {
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
errorCode = "userAlreadyExistsWeb";
|
errorCode = "userAlreadyExistsWeb";
|
||||||
}
|
}
|
||||||
logger.error("OAuth2 Authentication error: " + errorCode);
|
log.error("OAuth2 Authentication error: " + errorCode);
|
||||||
|
log.error("OAuth2AuthenticationException", exception);
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
return;
|
return;
|
||||||
} else if (exception instanceof LockedException) {
|
}
|
||||||
logger.error("Account locked: ", exception);
|
log.error("Unhandled authentication exception", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
logger.error("Unhandled authentication exception", exception);
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
@@ -26,9 +25,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
|
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
private ApplicationProperties applicationProperties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@@ -46,6 +42,17 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
String username = "";
|
||||||
|
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
|
OAuth2User oauthUser = (OAuth2User) principal;
|
||||||
|
username = oauthUser.getName();
|
||||||
|
} else if (principal instanceof UserDetails) {
|
||||||
|
UserDetails oauthUser = (UserDetails) principal;
|
||||||
|
username = oauthUser.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
@@ -59,11 +66,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
|
||||||
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
String username = oauthUser.getName();
|
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
@@ -78,9 +82,16 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
} else {
|
}
|
||||||
try {
|
try {
|
||||||
|
if (oAuth.getBlockRegistration()
|
||||||
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
}
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
return;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -90,4 +101,3 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,34 +2,26 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.Provider;
|
import stirling.software.SPDF.model.Provider;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
private static final Logger logger =
|
|
||||||
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
|
|
||||||
|
|
||||||
private final SessionRegistry sessionRegistry;
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public CustomOAuth2LogoutSuccessHandler(
|
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
|
||||||
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
|
|
||||||
this.sessionRegistry = sessionRegistry;
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +34,15 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
String issuer = null;
|
String issuer = null;
|
||||||
String clientId = null;
|
String clientId = null;
|
||||||
|
|
||||||
|
if (authentication == null) {
|
||||||
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
||||||
|
} else {
|
||||||
|
super.onLogoutSuccess(request, response, authentication);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
@@ -53,9 +54,8 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
issuer = provider.getIssuer();
|
issuer = provider.getIssuer();
|
||||||
clientId = provider.getClientId();
|
clientId = provider.getClientId();
|
||||||
} catch (UnsupportedProviderException e) {
|
} catch (UnsupportedProviderException e) {
|
||||||
logger.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
issuer = oauth.getIssuer();
|
issuer = oauth.getIssuer();
|
||||||
@@ -70,18 +70,16 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
||||||
|
param = "erroroauth=oauth2_admin_blocked_user";
|
||||||
|
} else if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
param = "erroroauth=userIsDisabled";
|
||||||
|
} else if (request.getParameter("badcredentials") != null) {
|
||||||
|
param = "error=badcredentials";
|
||||||
}
|
}
|
||||||
|
|
||||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||||
|
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
if (session != null) {
|
|
||||||
String sessionId = session.getId();
|
|
||||||
sessionRegistry.removeSessionInformation(sessionId);
|
|
||||||
session.invalidate();
|
|
||||||
logger.info("Session invalidated: " + sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
// Add Keycloak specific logout URL if needed
|
// Add Keycloak specific logout URL if needed
|
||||||
@@ -92,13 +90,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
+ clientId
|
+ clientId
|
||||||
+ "&post_logout_redirect_uri="
|
+ "&post_logout_redirect_uri="
|
||||||
+ response.encodeRedirectURL(redirect_url);
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
break;
|
break;
|
||||||
case "github":
|
case "github":
|
||||||
// Add GitHub specific logout URL if needed
|
// Add GitHub specific logout URL if needed
|
||||||
String githubLogoutUrl = "https://github.com/logout";
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
response.sendRedirect(githubLogoutUrl);
|
response.sendRedirect(githubLogoutUrl);
|
||||||
break;
|
break;
|
||||||
case "google":
|
case "google":
|
||||||
@@ -106,13 +104,14 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
// String googleLogoutUrl =
|
// String googleLogoutUrl =
|
||||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
// + response.encodeRedirectURL(redirect_url);
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
log.info("Google does not have a specific logout URL");
|
||||||
|
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
// break;
|
// break;
|
||||||
default:
|
default:
|
||||||
String redirectUrl = request.getContextPath() + "/login?" + param;
|
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||||
response.sendRedirect(redirectUrl);
|
response.sendRedirect(defaultRedirectUrl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class CustomHttpSessionListener implements HttpSessionListener {
|
||||||
|
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionCreated(HttpSessionEvent se) {
|
||||||
|
log.info("Session created: " + se.getSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
|
log.info("Session destroyed: " + se.getSession().getId());
|
||||||
|
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class SessionPersistentRegistry implements SessionRegistry {
|
||||||
|
|
||||||
|
private final SessionRepository sessionRepository;
|
||||||
|
|
||||||
|
@Value("${server.servlet.session.timeout:30m}")
|
||||||
|
private Duration defaultMaxInactiveInterval;
|
||||||
|
|
||||||
|
public SessionPersistentRegistry(SessionRepository sessionRepository) {
|
||||||
|
this.sessionRepository = sessionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Object> getAllPrincipals() {
|
||||||
|
List<SessionEntity> sessions = sessionRepository.findAll();
|
||||||
|
List<Object> principals = new ArrayList<>();
|
||||||
|
for (SessionEntity session : sessions) {
|
||||||
|
principals.add(session.getPrincipalName());
|
||||||
|
}
|
||||||
|
return principals;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<SessionInformation> getAllSessions(
|
||||||
|
Object principal, boolean includeExpiredSessions) {
|
||||||
|
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||||
|
String principalName = null;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
principalName = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
principalName = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principalName != null) {
|
||||||
|
List<SessionEntity> sessionEntities =
|
||||||
|
sessionRepository.findByPrincipalName(principalName);
|
||||||
|
for (SessionEntity sessionEntity : sessionEntities) {
|
||||||
|
if (includeExpiredSessions || !sessionEntity.isExpired()) {
|
||||||
|
sessionInformations.add(
|
||||||
|
new SessionInformation(
|
||||||
|
sessionEntity.getPrincipalName(),
|
||||||
|
sessionEntity.getSessionId(),
|
||||||
|
sessionEntity.getLastRequest()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessionInformations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void registerNewSession(String sessionId, Object principal) {
|
||||||
|
String principalName = null;
|
||||||
|
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
principalName = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
principalName = (String) principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principalName != null) {
|
||||||
|
SessionEntity sessionEntity = new SessionEntity();
|
||||||
|
sessionEntity.setSessionId(sessionId);
|
||||||
|
sessionEntity.setPrincipalName(principalName);
|
||||||
|
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
||||||
|
sessionEntity.setExpired(false);
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void removeSessionInformation(String sessionId) {
|
||||||
|
sessionRepository.deleteById(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void refreshLastRequest(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionInformation getSessionInformation(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
return new SessionInformation(
|
||||||
|
sessionEntity.getPrincipalName(),
|
||||||
|
sessionEntity.getSessionId(),
|
||||||
|
sessionEntity.getLastRequest());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all non-expired sessions
|
||||||
|
public List<SessionEntity> getAllSessionsNotExpired() {
|
||||||
|
return sessionRepository.findByExpired(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all sessions
|
||||||
|
public List<SessionEntity> getAllSessions() {
|
||||||
|
return sessionRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark a session as expired
|
||||||
|
public void expireSession(String sessionId) {
|
||||||
|
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||||
|
if (sessionEntityOpt.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||||
|
sessionEntity.setExpired(true); // Set expired to true
|
||||||
|
sessionRepository.save(sessionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the maximum inactive interval for sessions
|
||||||
|
public int getMaxInactiveInterval() {
|
||||||
|
return (int) defaultMaxInactiveInterval.getSeconds();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a session entity by session ID
|
||||||
|
public SessionEntity getSessionEntity(String sessionId) {
|
||||||
|
return sessionRepository.findBySessionId(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update session details by principal name
|
||||||
|
public void updateSessionByPrincipalName(
|
||||||
|
String principalName, boolean expired, Date lastRequest) {
|
||||||
|
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the latest session for a given principal name
|
||||||
|
public Optional<SessionEntity> findLatestSession(String principalName) {
|
||||||
|
List<SessionEntity> allSessions = sessionRepository.findByPrincipalName(principalName);
|
||||||
|
if (allSessions.isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort sessions by lastRequest in descending order
|
||||||
|
Collections.sort(
|
||||||
|
allSessions,
|
||||||
|
new Comparator<SessionEntity>() {
|
||||||
|
@Override
|
||||||
|
public int compare(SessionEntity s1, SessionEntity s2) {
|
||||||
|
// Sort by lastRequest in descending order
|
||||||
|
return s2.getLastRequest().compareTo(s1.getLastRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The first session in the list is the latest session for the given principal name
|
||||||
|
return Optional.of(allSessions.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class SessionRegistryConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionRegistryImpl sessionRegistry() {
|
||||||
|
return new SessionRegistryImpl();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SessionPersistentRegistry sessionPersistentRegistry(
|
||||||
|
SessionRepository sessionRepository) {
|
||||||
|
return new SessionPersistentRegistry(sessionRepository);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import jakarta.transaction.Transactional;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SessionRepository extends JpaRepository<SessionEntity, String> {
|
||||||
|
List<SessionEntity> findByPrincipalName(String principalName);
|
||||||
|
|
||||||
|
List<SessionEntity> findByExpired(boolean expired);
|
||||||
|
|
||||||
|
SessionEntity findBySessionId(String sessionId);
|
||||||
|
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query(
|
||||||
|
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
|
||||||
|
void saveByPrincipalName(
|
||||||
|
@Param("expired") boolean expired,
|
||||||
|
@Param("lastRequest") Date lastRequest,
|
||||||
|
@Param("principalName") String principalName);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class SessionScheduled {
|
||||||
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Scheduled(cron = "0 0/5 * * * ?")
|
||||||
|
public void expireSessions() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
||||||
|
List<SessionInformation> sessionInformations =
|
||||||
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
for (SessionInformation sessionInformation : sessionInformations) {
|
||||||
|
Date lastRequest = sessionInformation.getLastRequest();
|
||||||
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
|
Instant expirationTime =
|
||||||
|
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
|
if (now.isAfter(expirationTime)) {
|
||||||
|
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
import stirling.software.SPDF.service.PdfImageRemovalService;
|
||||||
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
|
||||||
|
* from a PDF file to reduce its size.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
public class PdfImageRemovalController {
|
||||||
|
|
||||||
|
// Service for removing images from PDFs
|
||||||
|
@Autowired private PdfImageRemovalService pdfImageRemovalService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for dependency injection of PdfImageRemovalService.
|
||||||
|
*
|
||||||
|
* @param pdfImageRemovalService The service used for removing images from PDFs.
|
||||||
|
*/
|
||||||
|
public PdfImageRemovalController(PdfImageRemovalService pdfImageRemovalService) {
|
||||||
|
this.pdfImageRemovalService = pdfImageRemovalService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint to remove images from a PDF file.
|
||||||
|
*
|
||||||
|
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
|
||||||
|
* PDF file with a new name indicating that images were removed.
|
||||||
|
*
|
||||||
|
* @param file The PDF file with images to be removed.
|
||||||
|
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
|
||||||
|
* content type and filename.
|
||||||
|
* @throws IOException If an error occurs while processing the PDF file.
|
||||||
|
*/
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
||||||
|
@Operation(
|
||||||
|
summary = "Remove images from file to reduce the file size.",
|
||||||
|
description =
|
||||||
|
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
|
||||||
|
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
|
||||||
|
|
||||||
|
MultipartFile pdf = file.getFileInput();
|
||||||
|
|
||||||
|
// Convert the MultipartFile to a byte array
|
||||||
|
byte[] pdfBytes = pdf.getBytes();
|
||||||
|
|
||||||
|
// Load the PDF document from the byte array
|
||||||
|
PDDocument document = Loader.loadPDF(pdfBytes);
|
||||||
|
|
||||||
|
// Remove images from the PDF document using the service
|
||||||
|
PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
|
||||||
|
|
||||||
|
// Create a ByteArrayOutputStream to hold the modified PDF data
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
// Save the modified PDF document to the output stream
|
||||||
|
modifiedDocument.save(outputStream);
|
||||||
|
modifiedDocument.close();
|
||||||
|
|
||||||
|
// Generate a new filename for the modified PDF
|
||||||
|
String mergedFileName =
|
||||||
|
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_images.pdf";
|
||||||
|
|
||||||
|
// Convert the byte array to a web response and return it
|
||||||
|
return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -12,8 +13,8 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -30,6 +31,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
@@ -41,6 +44,8 @@ public class UserController {
|
|||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
|
@Autowired SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
||||||
@@ -203,9 +208,10 @@ public class UserController {
|
|||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam String username,
|
||||||
@RequestParam(name = "password") String password,
|
@RequestParam(name = "password", required = false) String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
|
@RequestParam(name = "authType") String authType,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange)
|
boolean forceChange)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
@@ -237,7 +243,15 @@ public class UserController {
|
|||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
||||||
|
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
||||||
|
} else {
|
||||||
|
if (password.isBlank()) {
|
||||||
|
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||||
|
}
|
||||||
userService.saveUser(username, password, role, forceChange);
|
userService.saveUser(username, password, role, forceChange);
|
||||||
|
}
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
@@ -247,7 +261,8 @@ public class UserController {
|
|||||||
public RedirectView changeRole(
|
public RedirectView changeRole(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
Authentication authentication) {
|
Authentication authentication)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
@@ -278,6 +293,60 @@ public class UserController {
|
|||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
|
|
||||||
|
return new RedirectView(
|
||||||
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@PostMapping("/admin/changeUserEnabled/{username}")
|
||||||
|
public RedirectView changeUserEnabled(
|
||||||
|
@PathVariable("username") String username,
|
||||||
|
@RequestParam("enabled") boolean enabled,
|
||||||
|
Authentication authentication)
|
||||||
|
throws IOException {
|
||||||
|
|
||||||
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
|
if (!userOpt.isPresent()) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
|
}
|
||||||
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
|
}
|
||||||
|
// Get the currently authenticated username
|
||||||
|
String currentUsername = authentication.getName();
|
||||||
|
|
||||||
|
// Check if the provided username matches the current session's username
|
||||||
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
|
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
||||||
|
}
|
||||||
|
User user = userOpt.get();
|
||||||
|
|
||||||
|
userService.changeUserEnabled(user, enabled);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
// Invalidate all sessions if the user is being disabled
|
||||||
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
|
String userNameP = "";
|
||||||
|
for (Object principal : principals) {
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionRegistry.getAllSessions(principal, false);
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
userNameP = ((UserDetails) principal).getUsername();
|
||||||
|
} else if (principal instanceof OAuth2User) {
|
||||||
|
userNameP = ((OAuth2User) principal).getName();
|
||||||
|
} else if (principal instanceof String) {
|
||||||
|
userNameP = (String) principal;
|
||||||
|
}
|
||||||
|
if (userNameP.equalsIgnoreCase(username)) {
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
@@ -285,7 +354,7 @@ public class UserController {
|
|||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public RedirectView deleteUser(
|
public RedirectView deleteUser(
|
||||||
@PathVariable(name = "username") String username, Authentication authentication) {
|
@PathVariable("username") String username, Authentication authentication) {
|
||||||
|
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||||
@@ -298,27 +367,18 @@ public class UserController {
|
|||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||||
}
|
}
|
||||||
invalidateUserSessions(username);
|
|
||||||
|
// Invalidate all sessions before deleting the user
|
||||||
|
List<SessionInformation> sessionsInformations =
|
||||||
|
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
|
||||||
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
|
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
|
||||||
|
}
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return new RedirectView("/addUsers", true);
|
return new RedirectView("/addUsers", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired private SessionRegistry sessionRegistry;
|
|
||||||
|
|
||||||
private void invalidateUserSessions(String username) {
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
|
||||||
if (userDetails.getUsername().equals(username)) {
|
|
||||||
for (SessionInformation session :
|
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
|
||||||
session.expireNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/get-api-key")
|
@PostMapping("/get-api-key")
|
||||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -21,6 +33,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -60,15 +74,92 @@ public class ConvertImgPDFController {
|
|||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
imageFormat.toUpperCase(),
|
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
|
||||||
colorTypeResult,
|
colorTypeResult,
|
||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
filename);
|
filename);
|
||||||
|
|
||||||
if (result == null || result.length == 0) {
|
if (result == null || result.length == 0) {
|
||||||
logger.error("resultant bytes for {} is null, error converting ", filename);
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
|
if (imageFormat.equalsIgnoreCase("webp")) {
|
||||||
|
// Write the output stream to a temp file
|
||||||
|
Path tempFile = Files.createTempFile("temp_png", ".png");
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
||||||
|
fos.write(result);
|
||||||
|
fos.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
String pythonVersion = "python3";
|
||||||
|
try {
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||||
|
} catch (IOException e) {
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(Arrays.asList("python", "--version"));
|
||||||
|
pythonVersion = "python";
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> command = new ArrayList<>();
|
||||||
|
command.add(pythonVersion);
|
||||||
|
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||||
|
|
||||||
|
// Create a temporary directory for the output WebP files
|
||||||
|
Path tempOutputDir = Files.createTempDirectory("webp_output");
|
||||||
|
if (singleImage) {
|
||||||
|
// Run the Python script to convert PNG to WebP
|
||||||
|
command.add(tempFile.toString());
|
||||||
|
command.add(tempOutputDir.toString());
|
||||||
|
command.add("--single");
|
||||||
|
} else {
|
||||||
|
// Save the uploaded PDF to a temporary file
|
||||||
|
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
|
||||||
|
file.transferTo(tempPdfPath.toFile());
|
||||||
|
// Run the Python script to convert PDF to WebP
|
||||||
|
command.add(tempPdfPath.toString());
|
||||||
|
command.add(tempOutputDir.toString());
|
||||||
|
}
|
||||||
|
command.add("--dpi");
|
||||||
|
command.add(dpi);
|
||||||
|
ProcessExecutorResult resultProcess =
|
||||||
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
||||||
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
|
// Find all WebP files in the output directory
|
||||||
|
List<Path> webpFiles =
|
||||||
|
Files.walk(tempOutputDir)
|
||||||
|
.filter(path -> path.toString().endsWith(".webp"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (webpFiles.isEmpty()) {
|
||||||
|
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
|
||||||
|
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bodyBytes = new byte[0];
|
||||||
|
|
||||||
|
if (webpFiles.size() == 1) {
|
||||||
|
// Return the single WebP file directly
|
||||||
|
Path webpFilePath = webpFiles.get(0);
|
||||||
|
bodyBytes = Files.readAllBytes(webpFilePath);
|
||||||
|
} else {
|
||||||
|
// Create a ZIP file containing all WebP images
|
||||||
|
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
|
||||||
|
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
|
||||||
|
for (Path webpFile : webpFiles) {
|
||||||
|
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
|
||||||
|
Files.copy(webpFile, zos);
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bodyBytes = zipOutputStream.toByteArray();
|
||||||
|
}
|
||||||
|
// Clean up the temporary files
|
||||||
|
Files.deleteIfExists(tempFile);
|
||||||
|
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||||
|
result = bodyBytes;
|
||||||
|
}
|
||||||
|
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
String docName = filename + "." + imageFormat;
|
String docName = filename + "." + imageFormat;
|
||||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ public class ConvertWebsiteToPDF {
|
|||||||
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate the URL is reachable
|
||||||
|
if (!GeneralUtils.isURLReachable(URL)) {
|
||||||
|
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
|
||||||
|
}
|
||||||
|
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.stream.IntStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -17,6 +17,7 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -50,31 +51,31 @@ public class BlankPageController {
|
|||||||
int threshold = request.getThreshold();
|
int threshold = request.getThreshold();
|
||||||
float whitePercent = request.getWhitePercent();
|
float whitePercent = request.getWhitePercent();
|
||||||
|
|
||||||
PDDocument document = null;
|
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
|
||||||
try {
|
|
||||||
document = Loader.loadPDF(inputFile.getBytes());
|
|
||||||
PDPageTree pages = document.getDocumentCatalog().getPages();
|
PDPageTree pages = document.getDocumentCatalog().getPages();
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
|
|
||||||
List<Integer> pagesToKeepIndex = new ArrayList<>();
|
List<PDPage> nonBlankPages = new ArrayList<>();
|
||||||
|
List<PDPage> blankPages = new ArrayList<>();
|
||||||
int pageIndex = 0;
|
int pageIndex = 0;
|
||||||
|
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (PDPage page : pages) {
|
for (PDPage page : pages) {
|
||||||
logger.info("checking page " + pageIndex);
|
logger.info("checking page {}", pageIndex);
|
||||||
textStripper.setStartPage(pageIndex + 1);
|
textStripper.setStartPage(pageIndex + 1);
|
||||||
textStripper.setEndPage(pageIndex + 1);
|
textStripper.setEndPage(pageIndex + 1);
|
||||||
String pageText = textStripper.getText(document);
|
String pageText = textStripper.getText(document);
|
||||||
boolean hasText = !pageText.trim().isEmpty();
|
boolean hasText = !pageText.trim().isEmpty();
|
||||||
|
|
||||||
Boolean blank = true;
|
boolean blank = true;
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
logger.info("page " + pageIndex + " has text, not blank");
|
logger.info("page {} has text, not blank", pageIndex);
|
||||||
blank = false;
|
blank = false;
|
||||||
} else {
|
} else {
|
||||||
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
logger.info("page " + pageIndex + " has image, running blank detection");
|
logger.info("page {} has image, running blank detection", pageIndex);
|
||||||
// Render image and save as temp file
|
// Render image and save as temp file
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
||||||
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
||||||
@@ -82,34 +83,57 @@ public class BlankPageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (blank) {
|
if (blank) {
|
||||||
logger.info("Skipping, Image was blank for page #" + pageIndex);
|
logger.info("Skipping, Image was blank for page #{}", pageIndex);
|
||||||
|
blankPages.add(page);
|
||||||
} else {
|
} else {
|
||||||
logger.info("page " + pageIndex + " has image which is not blank");
|
logger.info("page {} has image which is not blank", pageIndex);
|
||||||
pagesToKeepIndex.add(pageIndex);
|
nonBlankPages.add(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
}
|
}
|
||||||
// Remove pages not present in pagesToKeepIndex
|
|
||||||
List<Integer> pageIndices =
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
ZipOutputStream zos = new ZipOutputStream(baos);
|
||||||
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
|
||||||
for (Integer i : pageIndices) {
|
String filename =
|
||||||
if (!pagesToKeepIndex.contains(i)) {
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
pages.remove(i);
|
.replaceFirst("[.][^.]+$", "");
|
||||||
}
|
|
||||||
|
if (!nonBlankPages.isEmpty()) {
|
||||||
|
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
|
||||||
|
} else {
|
||||||
|
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
|
||||||
document,
|
createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
}
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "_blanksRemoved.pdf");
|
zos.close();
|
||||||
|
|
||||||
|
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||||
|
return WebResponseUtils.boasToWebResponse(
|
||||||
|
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("exception", e);
|
logger.error("exception", e);
|
||||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
} finally {
|
}
|
||||||
if (document != null) document.close();
|
}
|
||||||
|
|
||||||
|
public void createZipEntry(ZipOutputStream zos, List<PDPage> pages, String entryName)
|
||||||
|
throws IOException {
|
||||||
|
try (PDDocument document = new PDDocument()) {
|
||||||
|
|
||||||
|
for (PDPage page : pages) {
|
||||||
|
document.addPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipEntry zipEntry = new ZipEntry(entryName);
|
||||||
|
zos.putNextEntry(zipEntry);
|
||||||
|
document.save(zos);
|
||||||
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public class CompressController {
|
|||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("gs");
|
command.add("gs");
|
||||||
command.add("-sDEVICE=pdfwrite");
|
command.add("-sDEVICE=pdfwrite");
|
||||||
command.add("-dCompatibilityLevel=1.4");
|
command.add("-dCompatibilityLevel=1.5");
|
||||||
|
|
||||||
switch (optimizeLevel) {
|
switch (optimizeLevel) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.Graphics2D;
|
import java.awt.*;
|
||||||
import java.awt.Image;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -49,7 +52,7 @@ public class ExtractImagesController {
|
|||||||
description =
|
description =
|
||||||
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
|
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
|
||||||
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
||||||
throws IOException {
|
throws IOException, InterruptedException, ExecutionException {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String format = request.getFormat();
|
||||||
|
|
||||||
@@ -57,6 +60,9 @@ public class ExtractImagesController {
|
|||||||
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
|
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
|
// Determine if multithreading should be used based on PDF size or number of pages
|
||||||
|
boolean useMultithreading = shouldUseMultithreading(file, document);
|
||||||
|
|
||||||
// Create ByteArrayOutputStream to write zip file to byte array
|
// Create ByteArrayOutputStream to write zip file to byte array
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|
||||||
@@ -66,71 +72,51 @@ public class ExtractImagesController {
|
|||||||
// Set compression level
|
// Set compression level
|
||||||
zos.setLevel(Deflater.BEST_COMPRESSION);
|
zos.setLevel(Deflater.BEST_COMPRESSION);
|
||||||
|
|
||||||
int imageIndex = 1;
|
|
||||||
String filename =
|
String filename =
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "");
|
||||||
int pageNum = 0;
|
|
||||||
Set<Integer> processedImages = new HashSet<>();
|
Set<Integer> processedImages = new HashSet<>();
|
||||||
|
|
||||||
|
if (useMultithreading) {
|
||||||
|
// Executor service to handle multithreading
|
||||||
|
ExecutorService executor =
|
||||||
|
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
||||||
|
Set<Future<Void>> futures = new HashSet<>();
|
||||||
|
|
||||||
// Iterate over each page
|
// Iterate over each page
|
||||||
for (PDPage page : document.getPages()) {
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
++pageNum;
|
PDPage page = document.getPage(pgNum);
|
||||||
// Extract images from page
|
int pageNum = document.getPages().indexOf(page) + 1;
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
// Submit a task for processing each page
|
||||||
if (page.getResources().isImageXObject(name)) {
|
Future<Void> future =
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
executor.submit(
|
||||||
int imageHash = image.hashCode();
|
() -> {
|
||||||
if (processedImages.contains(imageHash)) {
|
extractImagesFromPage(
|
||||||
continue; // Skip already processed images
|
page, format, filename, pageNum, processedImages, zos);
|
||||||
}
|
return null;
|
||||||
processedImages.add(imageHash);
|
});
|
||||||
|
|
||||||
// Convert image to desired format
|
futures.add(future);
|
||||||
RenderedImage renderedImage = image.getImage();
|
|
||||||
BufferedImage bufferedImage = null;
|
|
||||||
if ("png".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_INT_ARGB);
|
|
||||||
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_INT_RGB);
|
|
||||||
} else if ("gif".equalsIgnoreCase(format)) {
|
|
||||||
bufferedImage =
|
|
||||||
new BufferedImage(
|
|
||||||
renderedImage.getWidth(),
|
|
||||||
renderedImage.getHeight(),
|
|
||||||
BufferedImage.TYPE_BYTE_INDEXED);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write image to zip file
|
// Wait for all tasks to complete
|
||||||
String imageName =
|
for (Future<Void> future : futures) {
|
||||||
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
future.get();
|
||||||
ZipEntry zipEntry = new ZipEntry(imageName);
|
|
||||||
zos.putNextEntry(zipEntry);
|
|
||||||
|
|
||||||
Graphics2D g = bufferedImage.createGraphics();
|
|
||||||
g.drawImage((Image) renderedImage, 0, 0, null);
|
|
||||||
g.dispose();
|
|
||||||
// Write image bytes to zip file
|
|
||||||
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(bufferedImage, format, imageBaos);
|
|
||||||
zos.write(imageBaos.toByteArray());
|
|
||||||
|
|
||||||
zos.closeEntry();
|
|
||||||
imageIndex++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close executor service
|
||||||
|
executor.shutdown();
|
||||||
|
} else {
|
||||||
|
// Single-threaded extraction
|
||||||
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
|
PDPage page = document.getPage(pgNum);
|
||||||
|
extractImagesFromPage(page, format, filename, pgNum + 1, processedImages, zos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close ZipOutputStream and PDDocument
|
// Close PDDocument and ZipOutputStream
|
||||||
zos.close();
|
|
||||||
document.close();
|
document.close();
|
||||||
|
zos.close();
|
||||||
|
|
||||||
// Create ByteArrayResource from byte array
|
// Create ByteArrayResource from byte array
|
||||||
byte[] zipContents = baos.toByteArray();
|
byte[] zipContents = baos.toByteArray();
|
||||||
@@ -138,4 +124,72 @@ public class ExtractImagesController {
|
|||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.boasToWebResponse(
|
||||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
|
||||||
|
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
|
||||||
|
long fileSizeInMB = file.getSize() / (1024 * 1024);
|
||||||
|
int numberOfPages = document.getPages().getCount();
|
||||||
|
return fileSizeInMB > 10 || numberOfPages > 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void extractImagesFromPage(
|
||||||
|
PDPage page,
|
||||||
|
String format,
|
||||||
|
String filename,
|
||||||
|
int pageNum,
|
||||||
|
Set<Integer> processedImages,
|
||||||
|
ZipOutputStream zos)
|
||||||
|
throws IOException {
|
||||||
|
if(page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (COSName name : page.getResources().getXObjectNames()) {
|
||||||
|
if (page.getResources().isImageXObject(name)) {
|
||||||
|
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||||
|
int imageHash = image.hashCode();
|
||||||
|
synchronized (processedImages) {
|
||||||
|
if (processedImages.contains(imageHash)) {
|
||||||
|
continue; // Skip already processed images
|
||||||
|
}
|
||||||
|
processedImages.add(imageHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderedImage renderedImage = image.getImage();
|
||||||
|
|
||||||
|
// Convert to standard RGB colorspace if needed
|
||||||
|
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
||||||
|
|
||||||
|
// Write image to zip file
|
||||||
|
String imageName = filename + "_" + imageHash + " (Page " + pageNum + ")." + format;
|
||||||
|
synchronized (zos) {
|
||||||
|
zos.putNextEntry(new ZipEntry(imageName));
|
||||||
|
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(bufferedImage, format, imageBaos);
|
||||||
|
zos.write(imageBaos.toByteArray());
|
||||||
|
zos.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
|
||||||
|
int width = renderedImage.getWidth();
|
||||||
|
int height = renderedImage.getHeight();
|
||||||
|
BufferedImage rgbImage;
|
||||||
|
|
||||||
|
if ("png".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
} else if ("gif".equalsIgnoreCase(format)) {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
|
||||||
|
} else {
|
||||||
|
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics2D g = rgbImage.createGraphics();
|
||||||
|
g.drawImage((Image) renderedImage, 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
return rgbImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -27,6 +26,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
@@ -37,10 +37,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OCRController {
|
public class OCRController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tessdata";
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
|
||||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -32,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import stirling.software.SPDF.model.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
import stirling.software.SPDF.pdf.TextFinder;
|
import stirling.software.SPDF.pdf.TextFinder;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -81,22 +75,9 @@ public class RedactController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (convertPDFToImage) {
|
if (convertPDFToImage) {
|
||||||
PDDocument imageDocument = new PDDocument();
|
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
|
||||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
|
||||||
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
|
||||||
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
|
|
||||||
imageDocument.addPage(newPage);
|
|
||||||
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
|
||||||
PDPageContentStream contentStream =
|
|
||||||
new PDPageContentStream(
|
|
||||||
imageDocument, newPage, AppendMode.APPEND, true, true);
|
|
||||||
contentStream.drawImage(pdImage, 0, 0);
|
|
||||||
contentStream.close();
|
|
||||||
}
|
|
||||||
document.close();
|
document.close();
|
||||||
document = imageDocument;
|
document = convertedPdf;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||||
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -60,6 +61,7 @@ public class WatermarkController {
|
|||||||
float opacity = request.getOpacity();
|
float opacity = request.getOpacity();
|
||||||
int widthSpacer = request.getWidthSpacer();
|
int widthSpacer = request.getWidthSpacer();
|
||||||
int heightSpacer = request.getHeightSpacer();
|
int heightSpacer = request.getHeightSpacer();
|
||||||
|
boolean convertPdfToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
@@ -104,6 +106,12 @@ public class WatermarkController {
|
|||||||
contentStream.close();
|
contentStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (convertPdfToImage) {
|
||||||
|
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
||||||
|
document.close();
|
||||||
|
document = convertedPdf;
|
||||||
|
}
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -23,11 +25,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
@@ -35,15 +40,20 @@ import stirling.software.SPDF.model.provider.KeycloakProvider;
|
|||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@Slf4j
|
||||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
|
@Autowired SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
|
||||||
|
// If the user is already authenticated, redirect them to the home page.
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
@@ -137,6 +147,13 @@ public class AccountWebController {
|
|||||||
break;
|
break;
|
||||||
case "invalid_id_token":
|
case "invalid_id_token":
|
||||||
erroroauth = "login.oauth2InvalidIdToken";
|
erroroauth = "login.oauth2InvalidIdToken";
|
||||||
|
break;
|
||||||
|
case "oauth2_admin_blocked_user":
|
||||||
|
erroroauth = "login.oauth2AdminBlockedUser";
|
||||||
|
break;
|
||||||
|
case "userIsDisabled":
|
||||||
|
erroroauth = "login.userIsDisabled";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -155,9 +172,6 @@ public class AccountWebController {
|
|||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@GetMapping("/addUsers")
|
@GetMapping("/addUsers")
|
||||||
public String showAddUserForm(
|
public String showAddUserForm(
|
||||||
@@ -166,6 +180,13 @@ public class AccountWebController {
|
|||||||
Iterator<User> iterator = allUsers.iterator();
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||||
|
|
||||||
|
// Map to store session information and user activity status
|
||||||
|
Map<String, Boolean> userSessions = new HashMap<>();
|
||||||
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
|
||||||
|
int activeUsers = 0;
|
||||||
|
int disabledUsers = 0;
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
User user = iterator.next();
|
User user = iterator.next();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
@@ -176,8 +197,72 @@ public class AccountWebController {
|
|||||||
break; // Break out of the inner loop once the user is removed
|
break; // Break out of the inner loop once the user is removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the user's session status and last request time
|
||||||
|
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||||
|
boolean hasActiveSession = false;
|
||||||
|
Date lastRequest = null;
|
||||||
|
|
||||||
|
Optional<SessionEntity> latestSession =
|
||||||
|
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||||
|
if (latestSession.isPresent()) {
|
||||||
|
SessionEntity sessionEntity = latestSession.get();
|
||||||
|
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
// Calculate session expiration and update session status accordingly
|
||||||
|
Instant expirationTime =
|
||||||
|
lastAccessedTime
|
||||||
|
.toInstant()
|
||||||
|
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
|
if (now.isAfter(expirationTime)) {
|
||||||
|
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||||
|
hasActiveSession = false;
|
||||||
|
} else {
|
||||||
|
hasActiveSession = !sessionEntity.isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRequest = sessionEntity.getLastRequest();
|
||||||
|
} else {
|
||||||
|
hasActiveSession = false;
|
||||||
|
lastRequest = new Date(0); // No session, set default last request time
|
||||||
|
}
|
||||||
|
|
||||||
|
userSessions.put(user.getUsername(), hasActiveSession);
|
||||||
|
userLastRequest.put(user.getUsername(), lastRequest);
|
||||||
|
|
||||||
|
if (hasActiveSession) {
|
||||||
|
activeUsers++;
|
||||||
|
}
|
||||||
|
if (!user.isEnabled()) {
|
||||||
|
disabledUsers++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort users by active status and last request date
|
||||||
|
List<User> sortedUsers =
|
||||||
|
allUsers.stream()
|
||||||
|
.sorted(
|
||||||
|
(u1, u2) -> {
|
||||||
|
boolean u1Active = userSessions.get(u1.getUsername());
|
||||||
|
boolean u2Active = userSessions.get(u2.getUsername());
|
||||||
|
|
||||||
|
if (u1Active && !u2Active) {
|
||||||
|
return -1;
|
||||||
|
} else if (!u1Active && u2Active) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
Date u1LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u1.getUsername(), new Date(0));
|
||||||
|
Date u2LastRequest =
|
||||||
|
userLastRequest.getOrDefault(
|
||||||
|
u2.getUsername(), new Date(0));
|
||||||
|
return u2LastRequest.compareTo(u1LastRequest);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
|
|
||||||
@@ -203,6 +288,9 @@ public class AccountWebController {
|
|||||||
case "invalidUsername":
|
case "invalidUsername":
|
||||||
addMessage = "invalidUsernameMessage";
|
addMessage = "invalidUsernameMessage";
|
||||||
break;
|
break;
|
||||||
|
case "invalidPassword":
|
||||||
|
addMessage = "invalidPasswordMessage";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -218,16 +306,24 @@ public class AccountWebController {
|
|||||||
case "downgradeCurrentUser":
|
case "downgradeCurrentUser":
|
||||||
changeMessage = "downgradeCurrentUserMessage";
|
changeMessage = "downgradeCurrentUserMessage";
|
||||||
break;
|
break;
|
||||||
|
case "disabledCurrentUser":
|
||||||
|
changeMessage = "disabledCurrentUserMessage";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
changeMessage = messageType;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
model.addAttribute("changeMessage", changeMessage);
|
model.addAttribute("changeMessage", changeMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("users", allUsers);
|
model.addAttribute("users", sortedUsers);
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
model.addAttribute("roleDetails", roleDetails);
|
model.addAttribute("roleDetails", roleDetails);
|
||||||
|
model.addAttribute("userSessions", userSessions);
|
||||||
|
model.addAttribute("userLastRequest", userLastRequest);
|
||||||
|
model.addAttribute("totalUsers", allUsers.size());
|
||||||
|
model.addAttribute("activeUsers", activeUsers);
|
||||||
|
model.addAttribute("disabledUsers", disabledUsers);
|
||||||
return "addUsers";
|
return "addUsers";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,7 +374,7 @@ public class AccountWebController {
|
|||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
// Handle JSON conversion error
|
||||||
logger.error("exception", e);
|
log.error("exception", e);
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,6 @@ public class ConverterWebController {
|
|||||||
return "convert/book-to-pdf";
|
return "convert/book-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
|
||||||
@GetMapping("/pdf-to-book")
|
|
||||||
@Hidden
|
|
||||||
public String convertPdfToBookForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pdf-to-book");
|
|
||||||
return "convert/pdf-to-book";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/img-to-pdf")
|
@GetMapping("/img-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertImgToPdfForm(Model model) {
|
public String convertImgToPdfForm(Model model) {
|
||||||
@@ -57,13 +49,6 @@ public class ConverterWebController {
|
|||||||
return "convert/url-to-pdf";
|
return "convert/url-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-img")
|
|
||||||
@Hidden
|
|
||||||
public String pdfToimgForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pdf-to-img");
|
|
||||||
return "convert/pdf-to-img";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/file-to-pdf")
|
@GetMapping("/file-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertToPdfForm(Model model) {
|
public String convertToPdfForm(Model model) {
|
||||||
@@ -73,6 +58,21 @@ public class ConverterWebController {
|
|||||||
|
|
||||||
// PDF TO......
|
// PDF TO......
|
||||||
|
|
||||||
|
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
||||||
|
@GetMapping("/pdf-to-book")
|
||||||
|
@Hidden
|
||||||
|
public String convertPdfToBookForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "pdf-to-book");
|
||||||
|
return "convert/pdf-to-book";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pdf-to-img")
|
||||||
|
@Hidden
|
||||||
|
public String pdfToimgForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "pdf-to-img");
|
||||||
|
return "convert/pdf-to-img";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/pdf-to-html")
|
@GetMapping("/pdf-to-html")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView pdfToHTML() {
|
public ModelAndView pdfToHTML() {
|
||||||
|
|||||||
@@ -310,4 +310,11 @@ public class GeneralWebController {
|
|||||||
model.addAttribute("currentPage", "auto-split-pdf");
|
model.addAttribute("currentPage", "auto-split-pdf");
|
||||||
return "auto-split-pdf";
|
return "auto-split-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/remove-image-pdf")
|
||||||
|
@Hidden
|
||||||
|
public String removeImagePdfForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "remove-image-pdf");
|
||||||
|
return "remove-image-pdf";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -14,10 +15,14 @@ import org.springframework.web.servlet.ModelAndView;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OtherWebController {
|
public class OtherWebController {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@GetMapping("/compress-pdf")
|
@GetMapping("/compress-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String compressPdfForm(Model model) {
|
public String compressPdfForm(Model model) {
|
||||||
@@ -97,7 +102,7 @@ public class OtherWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = "/usr/share/tessdata";
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ public class ApplicationProperties {
|
|||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private Boolean autoCreateUser = false;
|
private Boolean autoCreateUser = false;
|
||||||
|
private Boolean blockRegistration = false;
|
||||||
private String useAsUsername;
|
private String useAsUsername;
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
private String provider;
|
private String provider;
|
||||||
@@ -286,6 +287,14 @@ public class ApplicationProperties {
|
|||||||
this.autoCreateUser = autoCreateUser;
|
this.autoCreateUser = autoCreateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Boolean getBlockRegistration() {
|
||||||
|
return blockRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockRegistration(Boolean blockRegistration) {
|
||||||
|
this.blockRegistration = blockRegistration;
|
||||||
|
}
|
||||||
|
|
||||||
public String getUseAsUsername() {
|
public String getUseAsUsername() {
|
||||||
return useAsUsername;
|
return useAsUsername;
|
||||||
}
|
}
|
||||||
@@ -356,10 +365,14 @@ public class ApplicationProperties {
|
|||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
+ ", autoCreateUser="
|
+ ", autoCreateUser="
|
||||||
+ autoCreateUser
|
+ autoCreateUser
|
||||||
|
+ ", blockRegistration="
|
||||||
|
+ blockRegistration
|
||||||
+ ", useAsUsername="
|
+ ", useAsUsername="
|
||||||
+ useAsUsername
|
+ useAsUsername
|
||||||
+ ", provider="
|
+ ", provider="
|
||||||
+ provider
|
+ provider
|
||||||
|
+ ", client="
|
||||||
|
+ client
|
||||||
+ ", scopes="
|
+ ", scopes="
|
||||||
+ scopes
|
+ scopes
|
||||||
+ "]";
|
+ "]";
|
||||||
@@ -429,6 +442,15 @@ public class ApplicationProperties {
|
|||||||
private boolean showUpdate;
|
private boolean showUpdate;
|
||||||
private Boolean showUpdateOnlyAdmin;
|
private Boolean showUpdateOnlyAdmin;
|
||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
|
private String tessdataDir;
|
||||||
|
|
||||||
|
public String getTessdataDir() {
|
||||||
|
return tessdataDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTessdataDir(String tessdataDir) {
|
||||||
|
this.tessdataDir = tessdataDir;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isCustomHTMLFiles() {
|
public boolean isCustomHTMLFiles() {
|
||||||
return customHTMLFiles;
|
return customHTMLFiles;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
@@ -11,7 +13,9 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "authorities")
|
@Table(name = "authorities")
|
||||||
public class Authority {
|
public class Authority implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
public Authority() {}
|
public Authority() {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Data
|
||||||
|
@Table(name = "sessions")
|
||||||
|
public class SessionEntity implements Serializable {
|
||||||
|
@Id private String sessionId;
|
||||||
|
|
||||||
|
@Lob private String principalName;
|
||||||
|
|
||||||
|
private Date lastRequest;
|
||||||
|
|
||||||
|
private boolean expired;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -23,7 +24,9 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
public class User {
|
public class User implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public class ConvertToImageRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The output image format",
|
description = "The output image format",
|
||||||
allowableValues = {"png", "jpeg", "jpg", "gif"})
|
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"})
|
||||||
private String imageFormat;
|
private String imageFormat;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
|
|||||||
@@ -44,4 +44,7 @@ public class AddWatermarkRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(description = "The height spacer between watermark elements", example = "50")
|
@Schema(description = "The height spacer between watermark elements", example = "50")
|
||||||
private int heightSpacer;
|
private int heightSpacer;
|
||||||
|
|
||||||
|
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
||||||
|
private boolean convertPDFToImage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ public class GoogleProvider extends Provider {
|
|||||||
private Collection<String> scopes = new ArrayList<>();
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
private String useAsUsername = "email";
|
private String useAsUsername = "email";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIssuer() {
|
||||||
|
return new String();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setIssuer(String issuer) {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
return this.clientId;
|
return this.clientId;
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package stirling.software.SPDF.repository;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
|
||||||
|
@Repository
|
||||||
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
|
public interface AuthorityRepository extends JpaRepository<Authority, Long> {
|
||||||
// Set<Authority> findByUsername(String username);
|
// Set<Authority> findByUsername(String username);
|
||||||
Set<Authority> findByUser_Username(String username);
|
Set<Authority> findByUser_Username(String username);
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package stirling.software.SPDF.repository;
|
package stirling.software.SPDF.repository;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PersistentLogin;
|
import stirling.software.SPDF.model.PersistentLogin;
|
||||||
|
|
||||||
|
@Repository
|
||||||
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}
|
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {}
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ package stirling.software.SPDF.repository;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, String> {
|
@Repository
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
Optional<User> findByUsernameIgnoreCase(String username);
|
Optional<User> findByUsernameIgnoreCase(String username);
|
||||||
|
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
|
||||||
User findByApiKey(String apiKey);
|
Optional<User> findByApiKey(String apiKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/** Service class responsible for removing image objects from a PDF document. */
|
||||||
|
@Service
|
||||||
|
public class PdfImageRemovalService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all image objects from the provided PDF document.
|
||||||
|
*
|
||||||
|
* <p>This method iterates over each page in the document and removes any image XObjects found
|
||||||
|
* in the page's resources.
|
||||||
|
*
|
||||||
|
* @param document The PDF document from which images will be removed.
|
||||||
|
* @return The modified PDF document with images removed.
|
||||||
|
* @throws IOException If an error occurs while processing the PDF document.
|
||||||
|
*/
|
||||||
|
public PDDocument removeImagesFromPdf(PDDocument document) throws IOException {
|
||||||
|
// Iterate over each page in the PDF document
|
||||||
|
for (PDPage page : document.getPages()) {
|
||||||
|
PDResources resources = page.getResources();
|
||||||
|
// Collect the XObject names to remove
|
||||||
|
List<COSName> namesToRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
// Iterate over all XObject names in the page's resources
|
||||||
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
|
// Check if the XObject is an image
|
||||||
|
if (resources.isImageXObject(name)) {
|
||||||
|
// Collect the name for removal
|
||||||
|
namesToRemove.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now, modify the resources by removing the collected names
|
||||||
|
for (COSName name : namesToRemove) {
|
||||||
|
resources.put(name, (PDXObject) null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import java.nio.file.SimpleFileVisitor;
|
|||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -71,6 +73,21 @@ public class GeneralUtils {
|
|||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isURLReachable(String urlStr) {
|
||||||
|
try {
|
||||||
|
URL url = new URL(urlStr);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("HEAD");
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
return (200 <= responseCode && responseCode <= 399);
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
public static File multipartToFile(MultipartFile multipart) throws IOException {
|
||||||
@@ -95,16 +112,13 @@ public class GeneralUtils {
|
|||||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||||
try {
|
try {
|
||||||
if (sizeStr.endsWith("KB")) {
|
if (sizeStr.endsWith("KB")) {
|
||||||
return (long)
|
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024);
|
|
||||||
} else if (sizeStr.endsWith("MB")) {
|
} else if (sizeStr.endsWith("MB")) {
|
||||||
return (long)
|
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
|
||||||
* 1024
|
* 1024
|
||||||
* 1024);
|
* 1024);
|
||||||
} else if (sizeStr.endsWith("GB")) {
|
} else if (sizeStr.endsWith("GB")) {
|
||||||
return (long)
|
return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
||||||
(Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2))
|
|
||||||
* 1024
|
* 1024
|
||||||
* 1024
|
* 1024
|
||||||
* 1024);
|
* 1024);
|
||||||
@@ -170,13 +184,15 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
int n = 0;
|
int n = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
// Replace 'n' with the current value of n, correctly handling numbers before 'n'
|
// Replace 'n' with the current value of n, correctly handling numbers before
|
||||||
|
// 'n'
|
||||||
String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
|
String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
|
||||||
Double result = evaluator.evaluate(sanitizedExpression);
|
Double result = evaluator.evaluate(sanitizedExpression);
|
||||||
|
|
||||||
// Check if the result is null or not within bounds
|
// Check if the result is null or not within bounds
|
||||||
if (result == null || result <= 0 || result.intValue() > maxValue) {
|
if (result == null || result <= 0 || result.intValue() > maxValue) {
|
||||||
if (n != 0) break;
|
if (n != 0)
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
results.add(result.intValue());
|
results.add(result.intValue());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -341,6 +341,30 @@ public class PdfUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a given Pdf file to PDF-Image.
|
||||||
|
*
|
||||||
|
* @param document to be converted. Note: the caller is responsible for closing the document
|
||||||
|
* @return converted document to PDF-Image
|
||||||
|
* @throws IOException if conversion fails
|
||||||
|
*/
|
||||||
|
public static PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
|
||||||
|
PDDocument imageDocument = new PDDocument();
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
|
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||||
|
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
|
||||||
|
imageDocument.addPage(newPage);
|
||||||
|
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
|
||||||
|
contentStream.drawImage(pdImage, 0, 0);
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
return imageDocument;
|
||||||
|
}
|
||||||
|
|
||||||
private static BufferedImage prepareImageForPdfToImage(
|
private static BufferedImage prepareImageForPdfToImage(
|
||||||
int maxWidth, int height, String imageType) {
|
int maxWidth, int height, String imageType) {
|
||||||
BufferedImage combined;
|
BufferedImage combined;
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
|
|||||||
incorrectPasswordMessage=Current password is incorrect.
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
usernameExistsMessage=New Username already exists.
|
usernameExistsMessage=New Username already exists.
|
||||||
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
||||||
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
||||||
downgradeCurrentUserMessage=لا يمكن خفض دور المستخدم الحالي
|
downgradeCurrentUserMessage=لا يمكن خفض دور المستخدم الحالي
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=لا يمكن تخفيض دور المستخدم الحالي. وبالتالي، لن يظهر المستخدم الحالي.
|
downgradeCurrentUserLongMessage=لا يمكن تخفيض دور المستخدم الحالي. وبالتالي، لن يظهر المستخدم الحالي.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=User
|
|||||||
adminUserSettings.addUser=Add New User
|
adminUserSettings.addUser=Add New User
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Roles
|
adminUserSettings.roles=Roles
|
||||||
adminUserSettings.role=Role
|
adminUserSettings.role=Role
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
|
|||||||
adminUserSettings.submit=Save User
|
adminUserSettings.submit=Save User
|
||||||
adminUserSettings.changeUserRole=تغيير دور المستخدم
|
adminUserSettings.changeUserRole=تغيير دور المستخدم
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
|
|||||||
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
|
|||||||
login.signinTitle=Please sign in
|
login.signinTitle=Please sign in
|
||||||
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
|
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
|
||||||
login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2
|
login.oauth2AutoCreateDisabled=تم تعطيل مستخدم الإنشاء التلقائي لـ OAuth2
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (مسافة بين كل علامة مائي
|
|||||||
watermark.selectText.7=التعتيم (0٪ - 100٪):
|
watermark.selectText.7=التعتيم (0٪ - 100٪):
|
||||||
watermark.selectText.8=Watermark Type:
|
watermark.selectText.8=Watermark Type:
|
||||||
watermark.selectText.9=Watermark Image:
|
watermark.selectText.9=Watermark Image:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=إضافة علامة مائية
|
watermark.submit=إضافة علامة مائية
|
||||||
watermark.type.1=نص
|
watermark.type.1=نص
|
||||||
watermark.type.2=صورة
|
watermark.type.2=صورة
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
|
|||||||
error.githubSubmit=GitHub - Submit a ticket
|
error.githubSubmit=GitHub - Submit a ticket
|
||||||
error.discordSubmit=Discord - Submit Support post
|
error.discordSubmit=Discord - Submit Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=Потребителят не е намерен
|
|||||||
incorrectPasswordMessage=Текущата парола е неправилна.
|
incorrectPasswordMessage=Текущата парола е неправилна.
|
||||||
usernameExistsMessage=Новият потребител вече съществува.
|
usernameExistsMessage=Новият потребител вече съществува.
|
||||||
invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес.
|
invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител.
|
deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител.
|
||||||
deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито.
|
deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито.
|
||||||
downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител
|
downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан.
|
downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан.
|
||||||
userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител.
|
userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител.
|
||||||
userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител.
|
userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=Потребител
|
|||||||
adminUserSettings.addUser=Добавяне на нов потребител
|
adminUserSettings.addUser=Добавяне на нов потребител
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
|
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
|
||||||
adminUserSettings.roles=Роли
|
adminUserSettings.roles=Роли
|
||||||
adminUserSettings.role=Роля
|
adminUserSettings.role=Роля
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Принудете потребителя да п
|
|||||||
adminUserSettings.submit=Съхранете потребителя
|
adminUserSettings.submit=Съхранете потребителя
|
||||||
adminUserSettings.changeUserRole=Промяна на ролята на потребителя
|
adminUserSettings.changeUserRole=Промяна на ролята на потребителя
|
||||||
adminUserSettings.authenticated=Удостоверен
|
adminUserSettings.authenticated=Удостоверен
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Книга към PDF
|
|||||||
home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre
|
home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre
|
||||||
BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle
|
BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Вашият акаунт е заключен.
|
|||||||
login.signinTitle=Моля впишете се
|
login.signinTitle=Моля впишете се
|
||||||
login.ssoSignIn=Влизане чрез еднократно влизане
|
login.ssoSignIn=Влизане чрез еднократно влизане
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
|
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Заявката за оторизация не е намерена
|
login.oauth2RequestNotFound=Заявката за оторизация не е намерена
|
||||||
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
|
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
|
||||||
login.oauth2invalidRequest=Невалидна заявка
|
login.oauth2invalidRequest=Невалидна заявка
|
||||||
login.oauth2AccessDenied=Отказан достъп
|
login.oauth2AccessDenied=Отказан достъп
|
||||||
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
|
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
|
||||||
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
|
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=дължинаSpacer (Разстояние между в
|
|||||||
watermark.selectText.7=Непрозрачност (0% - 100%):
|
watermark.selectText.7=Непрозрачност (0% - 100%):
|
||||||
watermark.selectText.8=Тип воден знак:
|
watermark.selectText.8=Тип воден знак:
|
||||||
watermark.selectText.9=Изображение за воден знак:
|
watermark.selectText.9=Изображение за воден знак:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Добавяне на воден знак
|
watermark.submit=Добавяне на воден знак
|
||||||
watermark.type.1=Текст
|
watermark.type.1=Текст
|
||||||
watermark.type.2=Изображение
|
watermark.type.2=Изображение
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Копиране на проследяване на стека
|
|||||||
error.githubSubmit=GitHub - Изпратете запитване
|
error.githubSubmit=GitHub - Изпратете запитване
|
||||||
error.discordSubmit=Discord - Изпратете запитване за поддръжка
|
error.discordSubmit=Discord - Изпратете запитване за поддръжка
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
|
|||||||
incorrectPasswordMessage=Current password is incorrect.
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
usernameExistsMessage=New Username already exists.
|
usernameExistsMessage=New Username already exists.
|
||||||
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
||||||
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
||||||
downgradeCurrentUserMessage=No es pot reduir la funció de l'usuari actual
|
downgradeCurrentUserMessage=No es pot reduir la funció de l'usuari actual
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=No es pot baixar la funció de l'usuari actual. Per tant, no es mostrarà l'usuari actual.
|
downgradeCurrentUserLongMessage=No es pot baixar la funció de l'usuari actual. Per tant, no es mostrarà l'usuari actual.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=Usuari
|
|||||||
adminUserSettings.addUser=Afegir Usuari
|
adminUserSettings.addUser=Afegir Usuari
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Rols
|
adminUserSettings.roles=Rols
|
||||||
adminUserSettings.role=Rol
|
adminUserSettings.role=Rol
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
|
|||||||
adminUserSettings.submit=Desar Usuari
|
adminUserSettings.submit=Desar Usuari
|
||||||
adminUserSettings.changeUserRole=Canvia el rol de l'usuari
|
adminUserSettings.changeUserRole=Canvia el rol de l'usuari
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
|
|||||||
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Compte bloquejat
|
|||||||
login.signinTitle=Autenticat
|
login.signinTitle=Autenticat
|
||||||
login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún
|
login.ssoSignIn=Inicia sessió mitjançant l'inici de sessió ún
|
||||||
login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat
|
login.oauth2AutoCreateDisabled=L'usuari de creació automàtica OAUTH2 està desactivat
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=separació d'alçada (Espai vertical entre cada Marca d'A
|
|||||||
watermark.selectText.7=Opacitat (0% - 100%):
|
watermark.selectText.7=Opacitat (0% - 100%):
|
||||||
watermark.selectText.8=Watermark Type:
|
watermark.selectText.8=Watermark Type:
|
||||||
watermark.selectText.9=Watermark Image:
|
watermark.selectText.9=Watermark Image:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Afegir Marca d'Aigua
|
watermark.submit=Afegir Marca d'Aigua
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Image
|
watermark.type.2=Image
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
|
|||||||
error.githubSubmit=GitHub - Submit a ticket
|
error.githubSubmit=GitHub - Submit a ticket
|
||||||
error.discordSubmit=Discord - Submit Support post
|
error.discordSubmit=Discord - Submit Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=Uživatel nenalezen.
|
|||||||
incorrectPasswordMessage=Současné heslo není správné.
|
incorrectPasswordMessage=Současné heslo není správné.
|
||||||
usernameExistsMessage=Nové uživatelské jméno již existuje.
|
usernameExistsMessage=Nové uživatelské jméno již existuje.
|
||||||
invalidUsernameMessage=Nesprávné uživatelské jméno, smí obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být validní emailová adresa.
|
invalidUsernameMessage=Nesprávné uživatelské jméno, smí obsahovat pouze písmena, číslice a následující speciální znaky @._+- nebo musí být validní emailová adresa.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Nelze smazat aktuální přihlášeného uživatele.
|
deleteCurrentUserMessage=Nelze smazat aktuální přihlášeného uživatele.
|
||||||
deleteUsernameExistsMessage=Uživatelské jméno neexistuje a nelze ho smazat.
|
deleteUsernameExistsMessage=Uživatelské jméno neexistuje a nelze ho smazat.
|
||||||
downgradeCurrentUserMessage=Nelze snížit roli aktuálního uživatele.
|
downgradeCurrentUserMessage=Nelze snížit roli aktuálního uživatele.
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Nelze snížit roli aktuálního uživatele. Proto nebude aktuální uživatel zobrazen.
|
downgradeCurrentUserLongMessage=Nelze snížit roli aktuálního uživatele. Proto nebude aktuální uživatel zobrazen.
|
||||||
userAlreadyExistsOAuthMessage=Uživatel již existuje jako OAuth2 uživatel.
|
userAlreadyExistsOAuthMessage=Uživatel již existuje jako OAuth2 uživatel.
|
||||||
userAlreadyExistsWebMessage=Uživatel již existuje jako webový uživatel.
|
userAlreadyExistsWebMessage=Uživatel již existuje jako webový uživatel.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=Uživatel
|
|||||||
adminUserSettings.addUser=Přidat Nového Uživatele
|
adminUserSettings.addUser=Přidat Nového Uživatele
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Uživatelské Jméno může obsahovat pouze písmena, čísla a následující speciální znaky @._+- nebo musí být správná emailová adresa.
|
adminUserSettings.usernameInfo=Uživatelské Jméno může obsahovat pouze písmena, čísla a následující speciální znaky @._+- nebo musí být správná emailová adresa.
|
||||||
adminUserSettings.roles=Role
|
adminUserSettings.roles=Role
|
||||||
adminUserSettings.role=Role
|
adminUserSettings.role=Role
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Vynutit uživateli změnu hesla při přihlášen
|
|||||||
adminUserSettings.submit=Uložit Uživatele
|
adminUserSettings.submit=Uložit Uživatele
|
||||||
adminUserSettings.changeUserRole=Zmenit Roli Uživatele
|
adminUserSettings.changeUserRole=Zmenit Roli Uživatele
|
||||||
adminUserSettings.authenticated=Ověřeno
|
adminUserSettings.authenticated=Ověřeno
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Kniha na PDF
|
|||||||
home.BookToPDF.desc=Převádí formáty knih/komiksů do PDF pomocí calibre
|
home.BookToPDF.desc=Převádí formáty knih/komiksů do PDF pomocí calibre
|
||||||
BookToPDF.tags=Kniha,Komiks,Calibre,Konvertovat,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
|
BookToPDF.tags=Kniha,Komiks,Calibre,Konvertovat,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
|
|||||||
login.signinTitle=Please sign in
|
login.signinTitle=Please sign in
|
||||||
login.ssoSignIn=Login via Single Sign-on
|
login.ssoSignIn=Login via Single Sign-on
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=Výška mezery (Mezera mezi každým vodoznakem svisle):
|
|||||||
watermark.selectText.7=Průhlednost (0% - 100%):
|
watermark.selectText.7=Průhlednost (0% - 100%):
|
||||||
watermark.selectText.8=Typ vodoznaku:
|
watermark.selectText.8=Typ vodoznaku:
|
||||||
watermark.selectText.9=Obrázek vodoznaku:
|
watermark.selectText.9=Obrázek vodoznaku:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Přidat vodoznak
|
watermark.submit=Přidat vodoznak
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Obrázek
|
watermark.type.2=Obrázek
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Kopírovat stopu zásobníku
|
|||||||
error.githubSubmit=GitHub - Odeslat požadavek
|
error.githubSubmit=GitHub - Odeslat požadavek
|
||||||
error.discordSubmit=Discord - Odeslat příspěvek podpory
|
error.discordSubmit=Discord - Odeslat příspěvek podpory
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
1150
src/main/resources/messages_da_DK.properties
Normal file
1150
src/main/resources/messages_da_DK.properties
Normal file
File diff suppressed because it is too large
Load Diff
@@ -55,10 +55,12 @@ userNotFoundMessage=Benutzer nicht gefunden.
|
|||||||
incorrectPasswordMessage=Das Passwort ist falsch.
|
incorrectPasswordMessage=Das Passwort ist falsch.
|
||||||
usernameExistsMessage=Neuer Benutzername existiert bereits.
|
usernameExistsMessage=Neuer Benutzername existiert bereits.
|
||||||
invalidUsernameMessage=Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
|
invalidUsernameMessage=Ungültiger Benutzername. Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
|
||||||
|
invalidPasswordMessage=Das Passwort darf nicht leer sein und kein Leerzeichen am Anfang und Ende haben.
|
||||||
confirmPasswordErrorMessage=„Neues Passwort“ und „Neues Passwort bestätigen“ müssen übereinstimmen.
|
confirmPasswordErrorMessage=„Neues Passwort“ und „Neues Passwort bestätigen“ müssen übereinstimmen.
|
||||||
deleteCurrentUserMessage=Der aktuell angemeldete Benutzer kann nicht gelöscht werden.
|
deleteCurrentUserMessage=Der aktuell angemeldete Benutzer kann nicht gelöscht werden.
|
||||||
deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden.
|
deleteUsernameExistsMessage=Der Benutzername existiert nicht und kann nicht gelöscht werden.
|
||||||
downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden
|
downgradeCurrentUserMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden
|
||||||
|
disabledCurrentUserMessage=Der aktuelle Benutzer kann nicht deaktiviert werden
|
||||||
downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt.
|
downgradeCurrentUserLongMessage=Die Rolle des aktuellen Benutzers kann nicht herabgestuft werden. Daher wird der aktuelle Benutzer nicht angezeigt.
|
||||||
userAlreadyExistsOAuthMessage=Der Benutzer ist bereits als OAuth2-Benutzer vorhanden.
|
userAlreadyExistsOAuthMessage=Der Benutzer ist bereits als OAuth2-Benutzer vorhanden.
|
||||||
userAlreadyExistsWebMessage=Der Benutzer ist bereits als Webbenutzer vorhanden.
|
userAlreadyExistsWebMessage=Der Benutzer ist bereits als Webbenutzer vorhanden.
|
||||||
@@ -177,10 +179,11 @@ adminUserSettings.user=Benutzer
|
|||||||
adminUserSettings.addUser=Neuen Benutzer hinzufügen
|
adminUserSettings.addUser=Neuen Benutzer hinzufügen
|
||||||
adminUserSettings.deleteUser=Benutzer löschen
|
adminUserSettings.deleteUser=Benutzer löschen
|
||||||
adminUserSettings.confirmDeleteUser=Soll der Benutzer gelöscht werden?
|
adminUserSettings.confirmDeleteUser=Soll der Benutzer gelöscht werden?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
|
adminUserSettings.usernameInfo=Der Benutzername darf nur Buchstaben, Zahlen und die folgenden Sonderzeichen @._+- enthalten oder muss eine gültige E-Mail-Adresse sein.
|
||||||
adminUserSettings.roles=Rollen
|
adminUserSettings.roles=Rollen
|
||||||
adminUserSettings.role=Rolle
|
adminUserSettings.role=Rolle
|
||||||
adminUserSettings.actions=Aktion
|
adminUserSettings.actions=Aktions
|
||||||
adminUserSettings.apiUser=Eingeschränkter API-Benutzer
|
adminUserSettings.apiUser=Eingeschränkter API-Benutzer
|
||||||
adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer
|
adminUserSettings.extraApiUser=Zusätzlicher eingeschränkter API-Benutzer
|
||||||
adminUserSettings.webOnlyUser=Nur Web-Benutzer
|
adminUserSettings.webOnlyUser=Nur Web-Benutzer
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei
|
|||||||
adminUserSettings.submit=Benutzer speichern
|
adminUserSettings.submit=Benutzer speichern
|
||||||
adminUserSettings.changeUserRole=Benutzerrolle ändern
|
adminUserSettings.changeUserRole=Benutzerrolle ändern
|
||||||
adminUserSettings.authenticated=Authentifiziert
|
adminUserSettings.authenticated=Authentifiziert
|
||||||
|
adminUserSettings.editOwnProfil=Eigenes Profil bearbeiten
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Datenbank Import/Export
|
database.title=Datenbank Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Buch als PDF
|
|||||||
home.BookToPDF.desc=Konvertiert Buch-/Comic-Formate mithilfe von Calibre in PDF
|
home.BookToPDF.desc=Konvertiert Buch-/Comic-Formate mithilfe von Calibre in PDF
|
||||||
BookToPDF.tags=buch,comic,calibre,convert,manga,amazon,kindle
|
BookToPDF.tags=buch,comic,calibre,convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Ihr Konto wurde gesperrt.
|
|||||||
login.signinTitle=Bitte melden Sie sich an.
|
login.signinTitle=Bitte melden Sie sich an.
|
||||||
login.ssoSignIn=Anmeldung per Single Sign-On
|
login.ssoSignIn=Anmeldung per Single Sign-On
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
|
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
|
||||||
|
login.oauth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
||||||
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
|
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
|
||||||
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
|
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
|
||||||
login.oauth2invalidRequest=ungültige Anfrage
|
login.oauth2invalidRequest=ungültige Anfrage
|
||||||
login.oauth2AccessDenied=Zugriff abgelehnt
|
login.oauth2AccessDenied=Zugriff abgelehnt
|
||||||
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
|
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
|
||||||
login.oauth2InvalidIdToken=Ungültiges ID-Token
|
login.oauth2InvalidIdToken=Ungültiges ID-Token
|
||||||
|
login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benutzernamen derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=höheSpacer (vertikaler Abstand zwischen den einzelnen Wa
|
|||||||
watermark.selectText.7=Deckkraft (0% - 100 %):
|
watermark.selectText.7=Deckkraft (0% - 100 %):
|
||||||
watermark.selectText.8=Wasserzeichen Typ:
|
watermark.selectText.8=Wasserzeichen Typ:
|
||||||
watermark.selectText.9=Wasserzeichen-Bild:
|
watermark.selectText.9=Wasserzeichen-Bild:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Wasserzeichen hinzufügen
|
watermark.submit=Wasserzeichen hinzufügen
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Bild
|
watermark.type.2=Bild
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Stack-Trace kopieren
|
|||||||
error.githubSubmit=GitHub - Ein Ticket einreichen
|
error.githubSubmit=GitHub - Ein Ticket einreichen
|
||||||
error.discordSubmit=Discord - Unterstützungsbeitrag einreichen
|
error.discordSubmit=Discord - Unterstützungsbeitrag einreichen
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=Ο χρήστης δεν βρέθηκε.
|
|||||||
incorrectPasswordMessage=Ο τρέχων κωδικός πρόσβασης είναι λανθασμένος.
|
incorrectPasswordMessage=Ο τρέχων κωδικός πρόσβασης είναι λανθασμένος.
|
||||||
usernameExistsMessage=Το νέο όνομα χρήστη υπάρχει ήδη.
|
usernameExistsMessage=Το νέο όνομα χρήστη υπάρχει ήδη.
|
||||||
invalidUsernameMessage=Μη έγκυρο όνομα χρήστη, όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ακόλουθους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email.
|
invalidUsernameMessage=Μη έγκυρο όνομα χρήστη, όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς και τους ακόλουθους ειδικούς χαρακτήρες @._+- ή πρέπει να είναι έγκυρη διεύθυνση email.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Δεν είναι δυνατή η διαγραφή του τρέχοντος συνδεδεμένου χρήστη.
|
deleteCurrentUserMessage=Δεν είναι δυνατή η διαγραφή του τρέχοντος συνδεδεμένου χρήστη.
|
||||||
deleteUsernameExistsMessage=Το όνομα χρήστη δεν υπάρχει και δεν μπορεί να διαγραφεί.
|
deleteUsernameExistsMessage=Το όνομα χρήστη δεν υπάρχει και δεν μπορεί να διαγραφεί.
|
||||||
downgradeCurrentUserMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη
|
downgradeCurrentUserMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη. Ως εκ τούτου, ο τρέχων χρήστης δεν θα εμφανίζεται.
|
downgradeCurrentUserLongMessage=Δεν είναι δυνατή η υποβάθμιση του ρόλου του τρέχοντος χρήστη. Ως εκ τούτου, ο τρέχων χρήστης δεν θα εμφανίζεται.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=Χρήστης
|
|||||||
adminUserSettings.addUser=Προσθήκη νέου Χρήστη
|
adminUserSettings.addUser=Προσθήκη νέου Χρήστη
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Ρόλοι
|
adminUserSettings.roles=Ρόλοι
|
||||||
adminUserSettings.role=Ρόλος
|
adminUserSettings.role=Ρόλος
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Αναγκάστε τον χρήστη να αλλ
|
|||||||
adminUserSettings.submit=Αποθήκευση Χρήστη
|
adminUserSettings.submit=Αποθήκευση Χρήστη
|
||||||
adminUserSettings.changeUserRole=Αλλαγή ρόλου χρήστη
|
adminUserSettings.changeUserRole=Αλλαγή ρόλου χρήστη
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book σε PDF
|
|||||||
home.BookToPDF.desc=Μετατρέπει τις μορφές Books/Comics σε PDF χρησιμοποιώντας calibre
|
home.BookToPDF.desc=Μετατρέπει τις μορφές Books/Comics σε PDF χρησιμοποιώντας calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
|
|||||||
login.signinTitle=Παρακαλώ, συνδεθείτε
|
login.signinTitle=Παρακαλώ, συνδεθείτε
|
||||||
login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης
|
login.ssoSignIn=Σύνδεση μέσω μοναδικής σύνδεσης
|
||||||
login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2
|
login.oauth2AutoCreateDisabled=Απενεργοποιήθηκε ο χρήστης αυτόματης δημιουργίας OAUTH2
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογ
|
|||||||
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
|
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
|
||||||
watermark.selectText.8=Τύπος Υδατογραφήματος:
|
watermark.selectText.8=Τύπος Υδατογραφήματος:
|
||||||
watermark.selectText.9=Εικόνα Υδατογραφήματος:
|
watermark.selectText.9=Εικόνα Υδατογραφήματος:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Προσθήκη Υδατογραφήματος
|
watermark.submit=Προσθήκη Υδατογραφήματος
|
||||||
watermark.type.1=Κείμενο
|
watermark.type.1=Κείμενο
|
||||||
watermark.type.2=Εικόνα
|
watermark.type.2=Εικόνα
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Αντιγραφή Stack Trace
|
|||||||
error.githubSubmit=GitHub - Υποβάλετε ένα ticket
|
error.githubSubmit=GitHub - Υποβάλετε ένα ticket
|
||||||
error.discordSubmit=Discord - Υποβάλετε ένα Support post
|
error.discordSubmit=Discord - Υποβάλετε ένα Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
|
|||||||
incorrectPasswordMessage=Current password is incorrect.
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
usernameExistsMessage=New Username already exists.
|
usernameExistsMessage=New Username already exists.
|
||||||
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
||||||
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
||||||
downgradeCurrentUserMessage=Cannot downgrade current user's role
|
downgradeCurrentUserMessage=Cannot downgrade current user's role
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
|
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=User
|
|||||||
adminUserSettings.addUser=Add New User
|
adminUserSettings.addUser=Add New User
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Roles
|
adminUserSettings.roles=Roles
|
||||||
adminUserSettings.role=Role
|
adminUserSettings.role=Role
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
|
|||||||
adminUserSettings.submit=Save User
|
adminUserSettings.submit=Save User
|
||||||
adminUserSettings.changeUserRole=Change User's Role
|
adminUserSettings.changeUserRole=Change User's Role
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
|
|||||||
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
|
|||||||
login.signinTitle=Please sign in
|
login.signinTitle=Please sign in
|
||||||
login.ssoSignIn=Login via Single Sign-on
|
login.ssoSignIn=Login via Single Sign-on
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Space between each watermark vertically):
|
|||||||
watermark.selectText.7=Opacity (0% - 100%):
|
watermark.selectText.7=Opacity (0% - 100%):
|
||||||
watermark.selectText.8=Watermark Type:
|
watermark.selectText.8=Watermark Type:
|
||||||
watermark.selectText.9=Watermark Image:
|
watermark.selectText.9=Watermark Image:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Add Watermark
|
watermark.submit=Add Watermark
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Image
|
watermark.type.2=Image
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
|
|||||||
error.githubSubmit=GitHub - Submit a ticket
|
error.githubSubmit=GitHub - Submit a ticket
|
||||||
error.discordSubmit=Discord - Submit Support post
|
error.discordSubmit=Discord - Submit Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
|
|||||||
incorrectPasswordMessage=Current password is incorrect.
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
usernameExistsMessage=New Username already exists.
|
usernameExistsMessage=New Username already exists.
|
||||||
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
||||||
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
||||||
downgradeCurrentUserMessage=Cannot downgrade current user's role
|
downgradeCurrentUserMessage=Cannot downgrade current user's role
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
|
downgradeCurrentUserLongMessage=Cannot downgrade current user's role. Hence, current user will not be shown.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=User
|
|||||||
adminUserSettings.addUser=Add New User
|
adminUserSettings.addUser=Add New User
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Roles
|
adminUserSettings.roles=Roles
|
||||||
adminUserSettings.role=Role
|
adminUserSettings.role=Role
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
|
|||||||
adminUserSettings.submit=Save User
|
adminUserSettings.submit=Save User
|
||||||
adminUserSettings.changeUserRole=Change User's Role
|
adminUserSettings.changeUserRole=Change User's Role
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
|
|||||||
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Your account has been locked.
|
|||||||
login.signinTitle=Please sign in
|
login.signinTitle=Please sign in
|
||||||
login.ssoSignIn=Login via Single Sign-on
|
login.ssoSignIn=Login via Single Sign-on
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=heightSpacer (Space between each watermark vertically):
|
|||||||
watermark.selectText.7=Opacity (0% - 100%):
|
watermark.selectText.7=Opacity (0% - 100%):
|
||||||
watermark.selectText.8=Watermark Type:
|
watermark.selectText.8=Watermark Type:
|
||||||
watermark.selectText.9=Watermark Image:
|
watermark.selectText.9=Watermark Image:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Add Watermark
|
watermark.submit=Add Watermark
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Image
|
watermark.type.2=Image
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
|
|||||||
error.githubSubmit=GitHub - Submit a ticket
|
error.githubSubmit=GitHub - Submit a ticket
|
||||||
error.discordSubmit=Discord - Submit Support post
|
error.discordSubmit=Discord - Submit Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,16 +55,18 @@ userNotFoundMessage=Usuario no encontrado.
|
|||||||
incorrectPasswordMessage=La contraseña actual no es correcta.
|
incorrectPasswordMessage=La contraseña actual no es correcta.
|
||||||
usernameExistsMessage=El nuevo nombre de usuario está en uso.
|
usernameExistsMessage=El nuevo nombre de usuario está en uso.
|
||||||
invalidUsernameMessage=Nombre de usuario no válido, el nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
|
invalidUsernameMessage=Nombre de usuario no válido, el nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=No puede eliminar el usuario que tiene la sesión actualmente en uso.
|
deleteCurrentUserMessage=No puede eliminar el usuario que tiene la sesión actualmente en uso.
|
||||||
deleteUsernameExistsMessage=El usuario no existe y no puede eliminarse.
|
deleteUsernameExistsMessage=El usuario no existe y no puede eliminarse.
|
||||||
downgradeCurrentUserMessage=No se puede degradar el rol del usuario actual
|
downgradeCurrentUserMessage=No se puede degradar el rol del usuario actual
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=No se puede degradar el rol del usuario actual. Por lo tanto, el usuario actual no se mostrará.
|
downgradeCurrentUserLongMessage=No se puede degradar el rol del usuario actual. Por lo tanto, el usuario actual no se mostrará.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=La usuario ya existe como usuario de OAuth2.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=El usuario ya existe como usuario web.
|
||||||
error=Error
|
error=Error
|
||||||
oops=Ups!
|
oops=Ups!
|
||||||
help=Help
|
help=Ayuda
|
||||||
goHomepage=Ir a la página principal
|
goHomepage=Ir a la página principal
|
||||||
joinDiscord=Únase a nuestro servidor Discord
|
joinDiscord=Únase a nuestro servidor Discord
|
||||||
seeDockerHub=Ver Docker Hub
|
seeDockerHub=Ver Docker Hub
|
||||||
@@ -86,7 +88,7 @@ pipeline.defaultOption=Personalizar
|
|||||||
pipeline.submitButton=Enviar
|
pipeline.submitButton=Enviar
|
||||||
pipeline.help=Ayuda de Canalización
|
pipeline.help=Ayuda de Canalización
|
||||||
pipeline.scanHelp=Ayuda de escaneado de carpetas
|
pipeline.scanHelp=Ayuda de escaneado de carpetas
|
||||||
pipeline.deletePrompt=Are you sure you want to delete pipeline
|
pipeline.deletePrompt=¿Estás segura de que quieres eliminar la canalización?
|
||||||
|
|
||||||
######################
|
######################
|
||||||
# Pipeline Options #
|
# Pipeline Options #
|
||||||
@@ -107,18 +109,18 @@ pipelineOptions.validateButton=Validar
|
|||||||
#############
|
#############
|
||||||
# NAVBAR #
|
# NAVBAR #
|
||||||
#############
|
#############
|
||||||
navbar.favorite=Favorites
|
navbar.favorite=Favoritos
|
||||||
navbar.darkmode=Modo oscuro
|
navbar.darkmode=Modo oscuro
|
||||||
navbar.language=Languages
|
navbar.language=Idiomas
|
||||||
navbar.settings=Configuración
|
navbar.settings=Configuración
|
||||||
navbar.allTools=Tools
|
navbar.allTools=Herramientas
|
||||||
navbar.multiTool=Multi Tools
|
navbar.multiTool=Multi herramientas
|
||||||
navbar.sections.organize=Organize
|
navbar.sections.organize=Organize
|
||||||
navbar.sections.convertTo=Convert to PDF
|
navbar.sections.convertTo=Convertir a PDF
|
||||||
navbar.sections.convertFrom=Convert from PDF
|
navbar.sections.convertFrom=Convertir desde PDF
|
||||||
navbar.sections.security=Sign & Security
|
navbar.sections.security=Señalización y seguridad
|
||||||
navbar.sections.advance=Advanced
|
navbar.sections.advance=Avanzado
|
||||||
navbar.sections.edit=View & Edit
|
navbar.sections.edit=Ver y Editar
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# SETTINGS #
|
# SETTINGS #
|
||||||
@@ -175,8 +177,9 @@ adminUserSettings.header=Configuración de control de usuario administrador
|
|||||||
adminUserSettings.admin=Administrador
|
adminUserSettings.admin=Administrador
|
||||||
adminUserSettings.user=Usuario
|
adminUserSettings.user=Usuario
|
||||||
adminUserSettings.addUser=Añadir Nuevo Usuario
|
adminUserSettings.addUser=Añadir Nuevo Usuario
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Eliminar Usuario
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=¿Se debe eliminar al usuario?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
|
adminUserSettings.usernameInfo=El nombre de usuario solo puede contener letras, números y los siguientes caracteres especiales @._+- o debe ser una dirección de correo electrónico válida.
|
||||||
adminUserSettings.roles=Roles
|
adminUserSettings.roles=Roles
|
||||||
adminUserSettings.role=Rol
|
adminUserSettings.role=Rol
|
||||||
@@ -189,24 +192,31 @@ adminUserSettings.internalApiUser=Usuario interno de API
|
|||||||
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
|
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
|
||||||
adminUserSettings.submit=Guardar Usuario
|
adminUserSettings.submit=Guardar Usuario
|
||||||
adminUserSettings.changeUserRole=Cambiar rol de usuario
|
adminUserSettings.changeUserRole=Cambiar rol de usuario
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Autenticado
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Base de Datos Importar/Exportar
|
||||||
database.header=Database Import/Export
|
database.header=Base de Datos Importar/Exportar
|
||||||
database.fileName=File Name
|
database.fileName=Nombre de Archivo
|
||||||
database.creationDate=Creation Date
|
database.creationDate=Fecha de creación
|
||||||
database.fileSize=File Size
|
database.fileSize=Tamaño de archivo
|
||||||
database.deleteBackupFile=Delete Backup File
|
database.deleteBackupFile=Eliminar archivo de copia de seguridad
|
||||||
database.importBackupFile=Import Backup File
|
database.importBackupFile=Importar archivo de copia de seguridad
|
||||||
database.downloadBackupFile=Download Backup File
|
database.downloadBackupFile=Descargar archivo de copia de seguridad
|
||||||
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application.
|
database.info_1=Al importar datos, es fundamental garantizar la estructura correcta. Si no está seguro de lo que está haciendo, busque consejo y apoyo de un profesional. Un error en la estructura puede causar un mal funcionamiento de la aplicación, incluyendo la imposibilidad total de ejecutar la aplicación.
|
||||||
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention.
|
database.info_2=El nombre del archivo no importa al cargarlo. Posteriormente se le cambiará el nombre para que siga el formato backup_user_yyyyMMddHHmm.sql, lo que garantiza una convención de nomenclatura coherente.
|
||||||
database.submit=Import Backup
|
database.submit=Importar Backup
|
||||||
database.importIntoDatabaseSuccessed=Import into database successed
|
database.importIntoDatabaseSuccessed=Importación a la base de datos ha sido exitosa
|
||||||
database.fileNotFound=File not Found
|
database.fileNotFound=Archivo no encontrado
|
||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Archivo de importación fallido
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
@@ -353,8 +363,8 @@ home.certSign.title=Firmar con certificado
|
|||||||
home.certSign.desc=Firmar un PDF con un Certificado/Clave (PEM/P12)
|
home.certSign.desc=Firmar un PDF con un Certificado/Clave (PEM/P12)
|
||||||
certSign.tags=autentificar,PEM,P12,oficial,encriptar
|
certSign.tags=autentificar,PEM,P12,oficial,encriptar
|
||||||
|
|
||||||
home.removeCertSign.title=Remove Certificate Sign
|
home.removeCertSign.title=Quitar signo de certificado
|
||||||
home.removeCertSign.desc=Remove certificate signature from PDF
|
home.removeCertSign.desc=Eliminar firma de certificado de PDF
|
||||||
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
|
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
|
||||||
|
|
||||||
home.pageLayout.title=Diseño de varias páginas
|
home.pageLayout.title=Diseño de varias páginas
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Libro a PDF
|
|||||||
home.BookToPDF.desc=Convierte formatos de Libro/Cómic a PDF usando Calibre
|
home.BookToPDF.desc=Convierte formatos de Libro/Cómic a PDF usando Calibre
|
||||||
BookToPDF.tags=Libro,Cómic,Calibre,Convertir,manga,Amazon,Kindle
|
BookToPDF.tags=Libro,Cómic,Calibre,Convertir,manga,Amazon,Kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -476,13 +490,15 @@ login.invalid=Nombre de usuario o contraseña erróneos.
|
|||||||
login.locked=Su cuenta se ha bloqueado.
|
login.locked=Su cuenta se ha bloqueado.
|
||||||
login.signinTitle=Por favor, inicie sesión
|
login.signinTitle=Por favor, inicie sesión
|
||||||
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
|
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
|
||||||
login.oauth2AutoCreateDisabled=Usuario DE creación automática de OAUTH2 DESACTIVADO
|
login.oauth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2RequestNotFound=Solicitud de autorización no encontrada
|
||||||
|
login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Respuesta de token no válida
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Token de identificación no válido
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -681,10 +697,10 @@ certSign.submit=Firmar PDF
|
|||||||
|
|
||||||
|
|
||||||
#removeCertSign
|
#removeCertSign
|
||||||
removeCertSign.title=Remove Certificate Signature
|
removeCertSign.title=Eliminar firma del certificado
|
||||||
removeCertSign.header=Remove the digital certificate from the PDF
|
removeCertSign.header=Quitar el certificado digital del PDF
|
||||||
removeCertSign.selectPDF=Select a PDF file:
|
removeCertSign.selectPDF=Seleccione un archivo PDF:
|
||||||
removeCertSign.submit=Remove Signature
|
removeCertSign.submit=Eliminar firma
|
||||||
|
|
||||||
|
|
||||||
#removeBlanks
|
#removeBlanks
|
||||||
@@ -706,8 +722,8 @@ removeAnnotations.submit=Eliminar
|
|||||||
#compare
|
#compare
|
||||||
compare.title=Comparar
|
compare.title=Comparar
|
||||||
compare.header=Comparar archivos PDF
|
compare.header=Comparar archivos PDF
|
||||||
compare.highlightColor.1=Highlight Color 1:
|
compare.highlightColor.1=Color resaltado 1:
|
||||||
compare.highlightColor.2=Highlight Color 2:
|
compare.highlightColor.2=Color resaltado 2:
|
||||||
compare.document.1=Documento 1
|
compare.document.1=Documento 1
|
||||||
compare.document.2=Documento 2
|
compare.document.2=Documento 2
|
||||||
compare.submit=Comparar
|
compare.submit=Comparar
|
||||||
@@ -744,7 +760,7 @@ repair.submit=Reparar
|
|||||||
#flatten
|
#flatten
|
||||||
flatten.title=Aplanar
|
flatten.title=Aplanar
|
||||||
flatten.header=Acoplar archivos PDF
|
flatten.header=Acoplar archivos PDF
|
||||||
flatten.flattenOnlyForms=Flatten only forms
|
flatten.flattenOnlyForms=Aplanar sólo formularios
|
||||||
flatten.submit=Aplanar
|
flatten.submit=Aplanar
|
||||||
|
|
||||||
|
|
||||||
@@ -792,7 +808,7 @@ extractImages.submit=Extraer
|
|||||||
fileToPDF.title=Archivo a PDF
|
fileToPDF.title=Archivo a PDF
|
||||||
fileToPDF.header=Convertir cualquier archivo a PDF
|
fileToPDF.header=Convertir cualquier archivo a PDF
|
||||||
fileToPDF.credit=Este servicio usa LibreOffice y Unoconv para la conversión de archivos
|
fileToPDF.credit=Este servicio usa LibreOffice y Unoconv para la conversión de archivos
|
||||||
fileToPDF.supportedFileTypesInfo=Supported File types
|
fileToPDF.supportedFileTypesInfo=Tipos de archivos admitidos
|
||||||
fileToPDF.supportedFileTypes=Los tipos de archivo soportados deben incluir los indicados a continuación; sin embargo, para una completa y acutualizada lista de formatos soportados, por favor consulte la documentación de LibreOffice
|
fileToPDF.supportedFileTypes=Los tipos de archivo soportados deben incluir los indicados a continuación; sin embargo, para una completa y acutualizada lista de formatos soportados, por favor consulte la documentación de LibreOffice
|
||||||
fileToPDF.submit=Convertir a PDF
|
fileToPDF.submit=Convertir a PDF
|
||||||
|
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=Alto (Espacio entre cada marca de agua verticalmente):
|
|||||||
watermark.selectText.7=Opacidad (0% - 100%):
|
watermark.selectText.7=Opacidad (0% - 100%):
|
||||||
watermark.selectText.8=Tipo de marca de agua:
|
watermark.selectText.8=Tipo de marca de agua:
|
||||||
watermark.selectText.9=Imagen de marca de agua:
|
watermark.selectText.9=Imagen de marca de agua:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Añadir marca de agua
|
watermark.submit=Añadir marca de agua
|
||||||
watermark.type.1=Texto
|
watermark.type.1=Texto
|
||||||
watermark.type.2=Imagen
|
watermark.type.2=Imagen
|
||||||
@@ -1000,8 +1017,8 @@ pdfToPDFA.header=PDF a PDF/A
|
|||||||
pdfToPDFA.credit=Este servicio usa OCRmyPDF para la conversión a PDF/A
|
pdfToPDFA.credit=Este servicio usa OCRmyPDF para la conversión a PDF/A
|
||||||
pdfToPDFA.submit=Convertir
|
pdfToPDFA.submit=Convertir
|
||||||
pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez
|
pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez
|
||||||
pdfToPDFA.outputFormat=Output format
|
pdfToPDFA.outputFormat=Formato de salida
|
||||||
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
|
pdfToPDFA.pdfWithDigitalSignature=El PDF contiene una firma digital. Esto se eliminará en el siguiente paso.
|
||||||
|
|
||||||
|
|
||||||
#PDFToWord
|
#PDFToWord
|
||||||
@@ -1103,13 +1120,13 @@ licenses.version=Versión
|
|||||||
licenses.license=Licencia
|
licenses.license=Licencia
|
||||||
|
|
||||||
#survey
|
#survey
|
||||||
survey.nav=Survey
|
survey.nav=Encuesta
|
||||||
survey.title=Stirling-PDF Survey
|
survey.title=Encuesta Stirling-PDF
|
||||||
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF!
|
survey.description=Stirling-PDF no tiene seguimiento, por lo que queremos escuchar a nuestros usuarios para mejorar Stirling-PDF.
|
||||||
survey.please=Please consider taking our survey!
|
survey.please=¡Considere realizar nuestra encuesta!
|
||||||
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page)
|
survey.disabled=(La ventana emergente de la encuesta se desactivará en las siguientes actualizaciones, pero estará disponible al pie de la página.)
|
||||||
survey.button=Take Survey
|
survey.button=Realizar encuesta
|
||||||
survey.dontShowAgain=Don't show again
|
survey.dontShowAgain=No volver a mostrar
|
||||||
|
|
||||||
|
|
||||||
#error
|
#error
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Mostrar seguimiento de pila
|
|||||||
error.githubSubmit=GitHub - Enviar un ticket
|
error.githubSubmit=GitHub - Enviar un ticket
|
||||||
error.discordSubmit=Discord - Enviar mensaje de soporte
|
error.discordSubmit=Discord - Enviar mensaje de soporte
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ userNotFoundMessage=User not found.
|
|||||||
incorrectPasswordMessage=Current password is incorrect.
|
incorrectPasswordMessage=Current password is incorrect.
|
||||||
usernameExistsMessage=New Username already exists.
|
usernameExistsMessage=New Username already exists.
|
||||||
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
invalidUsernameMessage=Invalid username, username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
|
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
|
||||||
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
|
||||||
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
deleteCurrentUserMessage=Cannot delete currently logged in user.
|
||||||
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
deleteUsernameExistsMessage=The username does not exist and cannot be deleted.
|
||||||
downgradeCurrentUserMessage=Ezin da uneko erabiltzailearen rola jaitsi
|
downgradeCurrentUserMessage=Ezin da uneko erabiltzailearen rola jaitsi
|
||||||
|
disabledCurrentUserMessage=The current user cannot be disabled
|
||||||
downgradeCurrentUserLongMessage=Ezin da uneko erabiltzailearen rola jaitsi. Beraz, oraingo erabiltzailea ez da erakutsiko.
|
downgradeCurrentUserLongMessage=Ezin da uneko erabiltzailearen rola jaitsi. Beraz, oraingo erabiltzailea ez da erakutsiko.
|
||||||
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
userAlreadyExistsOAuthMessage=The user already exists as an OAuth2 user.
|
||||||
userAlreadyExistsWebMessage=The user already exists as an web user.
|
userAlreadyExistsWebMessage=The user already exists as an web user.
|
||||||
@@ -177,6 +179,7 @@ adminUserSettings.user=Erabiltzaile
|
|||||||
adminUserSettings.addUser=Erabiltzaile berria
|
adminUserSettings.addUser=Erabiltzaile berria
|
||||||
adminUserSettings.deleteUser=Delete User
|
adminUserSettings.deleteUser=Delete User
|
||||||
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
adminUserSettings.confirmDeleteUser=Should the user be deleted?
|
||||||
|
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
|
||||||
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
adminUserSettings.usernameInfo=Username can only contain letters, numbers and the following special characters @._+- or must be a valid email address.
|
||||||
adminUserSettings.roles=Rolak
|
adminUserSettings.roles=Rolak
|
||||||
adminUserSettings.role=Rol
|
adminUserSettings.role=Rol
|
||||||
@@ -190,6 +193,13 @@ adminUserSettings.forceChange=Force user to change password on login
|
|||||||
adminUserSettings.submit=Gorde Erabiltzailea
|
adminUserSettings.submit=Gorde Erabiltzailea
|
||||||
adminUserSettings.changeUserRole=Erabiltzailearen rola aldatu
|
adminUserSettings.changeUserRole=Erabiltzailearen rola aldatu
|
||||||
adminUserSettings.authenticated=Authenticated
|
adminUserSettings.authenticated=Authenticated
|
||||||
|
adminUserSettings.editOwnProfil=Edit own profile
|
||||||
|
adminUserSettings.enabledUser=enabled user
|
||||||
|
adminUserSettings.disabledUser=disabled user
|
||||||
|
adminUserSettings.activeUsers=Active Users:
|
||||||
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
|
adminUserSettings.totalUsers=Total Users:
|
||||||
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
|
||||||
|
|
||||||
database.title=Database Import/Export
|
database.title=Database Import/Export
|
||||||
@@ -461,6 +471,10 @@ home.BookToPDF.title=Book to PDF
|
|||||||
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
home.BookToPDF.desc=Converts Books/Comics formats to PDF using calibre
|
||||||
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
|
||||||
|
|
||||||
|
home.removeImagePdf.title=Remove image
|
||||||
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
###########################
|
###########################
|
||||||
# #
|
# #
|
||||||
@@ -477,12 +491,14 @@ login.locked=Zure kontua blokeatu egin da.
|
|||||||
login.signinTitle=Mesedez, hasi saioa
|
login.signinTitle=Mesedez, hasi saioa
|
||||||
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
|
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
|
||||||
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
|
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
|
||||||
|
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||||
login.oauth2RequestNotFound=Authorization request not found
|
login.oauth2RequestNotFound=Authorization request not found
|
||||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||||
login.oauth2invalidRequest=Invalid Request
|
login.oauth2invalidRequest=Invalid Request
|
||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
@@ -945,6 +961,7 @@ watermark.selectText.6=Altuera (ur-marka bakoitzaren arteko espazioa bertikalean
|
|||||||
watermark.selectText.7=Opakutasuna (0% - 100%):
|
watermark.selectText.7=Opakutasuna (0% - 100%):
|
||||||
watermark.selectText.8=Watermark Type:
|
watermark.selectText.8=Watermark Type:
|
||||||
watermark.selectText.9=Watermark Image:
|
watermark.selectText.9=Watermark Image:
|
||||||
|
watermark.selectText.10=Convert PDF to PDF-Image
|
||||||
watermark.submit=Gehitu ur-marka
|
watermark.submit=Gehitu ur-marka
|
||||||
watermark.type.1=Text
|
watermark.type.1=Text
|
||||||
watermark.type.2=Image
|
watermark.type.2=Image
|
||||||
@@ -1125,3 +1142,9 @@ error.copyStack=Copy Stack Trace
|
|||||||
error.githubSubmit=GitHub - Submit a ticket
|
error.githubSubmit=GitHub - Submit a ticket
|
||||||
error.discordSubmit=Discord - Submit Support post
|
error.discordSubmit=Discord - Submit Support post
|
||||||
|
|
||||||
|
|
||||||
|
#remove-image
|
||||||
|
removeImage.title=Remove image
|
||||||
|
removeImage.header=Remove image
|
||||||
|
removeImage.removeImage=Remove image
|
||||||
|
removeImage.submit=Remove image
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user