Compare commits
2 Commits
stirling-p
...
load-pdf-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fb605baa1 | ||
|
|
7e2a53a02e |
17
.github/labeler-config.yml
vendored
17
.github/labeler-config.yml
vendored
@@ -16,27 +16,21 @@ Java:
|
|||||||
|
|
||||||
Back End:
|
Back End:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/**/*'
|
- 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/settings.yml.template'
|
||||||
- any-glob-to-any-file: 'src/main/resources/application.properties'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
||||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
|
||||||
- any-glob-to-any-file: 'split_photos.py'
|
|
||||||
|
|
||||||
Security:
|
Security:
|
||||||
- changed-files:
|
- 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/security/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
- 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/model/AuthenticationType.java'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
|
||||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
|
||||||
|
|
||||||
API:
|
API:
|
||||||
- changed-files:
|
- 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/web/MetricsController.java'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
||||||
- any-glob-to-any-file: 'scripts/png_to_webp.py'
|
|
||||||
- any-glob-to-any-file: 'split_photos.py'
|
|
||||||
|
|
||||||
Documentation:
|
Documentation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
@@ -49,9 +43,6 @@ Docker:
|
|||||||
- 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'
|
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
||||||
- any-glob-to-any-file: 'scripts/init.sh'
|
|
||||||
- any-glob-to-any-file: 'scripts/init-without-ocr.sh'
|
|
||||||
- any-glob-to-any-file: 'scripts/installFonts.sh'
|
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
21
.github/labels.yml
vendored
21
.github/labels.yml
vendored
@@ -3,12 +3,9 @@
|
|||||||
#
|
#
|
||||||
# The repository labels will be automatically configured using this file and
|
# The repository labels will be automatically configured using this file and
|
||||||
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
# the GitHub Action https://github.com/marketplace/actions/github-labeler.
|
||||||
- name: "Licenses"
|
|
||||||
color: "EDEDED"
|
|
||||||
from_name: "licenses"
|
|
||||||
- name: "Back End"
|
- name: "Back End"
|
||||||
color: "20CE6C"
|
color: "20CE6C"
|
||||||
description: "Issues or pull requests related to back-end development"
|
description: "Issues related to back-end development"
|
||||||
from_name: "Back end"
|
from_name: "Back end"
|
||||||
- name: "Bug"
|
- name: "Bug"
|
||||||
description: "Something isn't working"
|
description: "Something isn't working"
|
||||||
@@ -27,7 +24,6 @@
|
|||||||
from_name: "documentation"
|
from_name: "documentation"
|
||||||
- name: "Done for next release"
|
- name: "Done for next release"
|
||||||
color: "0CDBD1"
|
color: "0CDBD1"
|
||||||
description: "Items that are completed and will be included in the next release"
|
|
||||||
- name: "Done"
|
- name: "Done"
|
||||||
color: "60F13B"
|
color: "60F13B"
|
||||||
- name: "duplicate"
|
- name: "duplicate"
|
||||||
@@ -41,7 +37,7 @@
|
|||||||
description: "Fix needs to be confirmed"
|
description: "Fix needs to be confirmed"
|
||||||
- name: "Front End"
|
- name: "Front End"
|
||||||
color: "BBD2F1"
|
color: "BBD2F1"
|
||||||
description: "Issues or pull requests related to front-end development"
|
description: "Issues related to front-end development"
|
||||||
- name: "github-actions"
|
- name: "github-actions"
|
||||||
description: "Pull requests that update GitHub Actions code"
|
description: "Pull requests that update GitHub Actions code"
|
||||||
color: "999999"
|
color: "999999"
|
||||||
@@ -95,16 +91,3 @@
|
|||||||
description: "Testing-related issues or pull requests"
|
description: "Testing-related issues or pull requests"
|
||||||
- name: "Stale"
|
- name: "Stale"
|
||||||
color: "000000"
|
color: "000000"
|
||||||
description: "Issues or pull requests that have become inactive"
|
|
||||||
- name: "Priority: Critical"
|
|
||||||
color: "000000"
|
|
||||||
description: "Issues or pull requests with the highest priority"
|
|
||||||
- name: "Priority: High"
|
|
||||||
color: "FF0000"
|
|
||||||
description: "Issues or pull requests with high priority"
|
|
||||||
- name: "Priority: Medium"
|
|
||||||
color: "FFFF00"
|
|
||||||
description: "Issues or pull requests with medium priority"
|
|
||||||
- name: "Priority: Low"
|
|
||||||
color: "00FF00"
|
|
||||||
description: "Issues or pull requests with low priority"
|
|
||||||
|
|||||||
1
.github/pull_request_template.md
vendored
1
.github/pull_request_template.md
vendored
@@ -8,7 +8,6 @@ Closes #(issue_number)
|
|||||||
|
|
||||||
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] I have attached images of the change if it is UI based
|
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||||
- [ ] My changes generate no new warnings
|
- [ ] My changes generate no new warnings
|
||||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||||
|
|||||||
53
.github/scripts/gradle_to_chart.py
vendored
53
.github/scripts/gradle_to_chart.py
vendored
@@ -8,20 +8,17 @@ gradle_path = "build.gradle"
|
|||||||
|
|
||||||
def get_chart_version(path):
|
def get_chart_version(path):
|
||||||
"""
|
"""
|
||||||
Reads the version and the appVersion from Chart.yaml.
|
Reads the appVersion from Chart.yaml.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (str): The file path to the Chart.yaml.
|
path (str): The file path to the Chart.yaml.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: The version under "chart" key and the appVersion under "app" key.
|
str: The appVersion if found, otherwise an empty string.
|
||||||
"""
|
"""
|
||||||
with open(path, encoding="utf-8") as file:
|
with open(path, encoding="utf-8") as file:
|
||||||
chart_yaml = yaml.safe_load(file)
|
chart_yaml = yaml.safe_load(file)
|
||||||
return {
|
return chart_yaml.get("appVersion", "")
|
||||||
"chart": chart_yaml["version"],
|
|
||||||
"app": chart_yaml["appVersion"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_gradle_version(path):
|
def get_gradle_version(path):
|
||||||
@@ -42,46 +39,17 @@ def get_gradle_version(path):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_new_chart_version(chart_version, old_app_version, new_app_version):
|
def update_chart_version(path, new_version):
|
||||||
"""
|
|
||||||
Get the new chart version from
|
|
||||||
|
|
||||||
Args:
|
|
||||||
str: The current chart version.
|
|
||||||
str: The current app version.
|
|
||||||
str: The new app version.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The new chart version to update to.
|
|
||||||
"""
|
|
||||||
chart_major, chart_minor, chart_patch = chart_version.split(".")
|
|
||||||
|
|
||||||
old_major, old_minor, old_patch = old_app_version.split(".")
|
|
||||||
new_major, new_minor, new_patch = new_app_version.split(".")
|
|
||||||
|
|
||||||
if old_major != new_major:
|
|
||||||
new_chart_version = f"{int(chart_major)+1}.0.0"
|
|
||||||
elif old_minor != new_minor:
|
|
||||||
new_chart_version = f"{chart_major}.{int(chart_minor)+1}.0"
|
|
||||||
elif old_patch != new_patch:
|
|
||||||
new_chart_version = f"{chart_major}.{chart_minor}.{int(chart_patch)+1}"
|
|
||||||
|
|
||||||
return new_chart_version
|
|
||||||
|
|
||||||
|
|
||||||
def update_chart_version(path, new_chart_version, new_app_version):
|
|
||||||
"""
|
"""
|
||||||
Updates the version and the appVersion in Chart.yaml with a new version.
|
Updates the appVersion in Chart.yaml with a new version.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path (str): The file path to the Chart.yaml.
|
path (str): The file path to the Chart.yaml.
|
||||||
new_chart_version (str): The new chart version to update to.
|
new_version (str): The new version to update to.
|
||||||
new_app_version (str): The new app version to update to.
|
|
||||||
"""
|
"""
|
||||||
with open(path, encoding="utf-8") as file:
|
with open(path, encoding="utf-8") as file:
|
||||||
chart_yaml = yaml.safe_load(file)
|
chart_yaml = yaml.safe_load(file)
|
||||||
chart_yaml["version"] = new_chart_version
|
chart_yaml["appVersion"] = new_version
|
||||||
chart_yaml["appVersion"] = new_app_version
|
|
||||||
with open(path, "w", encoding="utf-8") as file:
|
with open(path, "w", encoding="utf-8") as file:
|
||||||
yaml.safe_dump(chart_yaml, file)
|
yaml.safe_dump(chart_yaml, file)
|
||||||
|
|
||||||
@@ -90,11 +58,10 @@ def update_chart_version(path, new_chart_version, new_app_version):
|
|||||||
chart_version = get_chart_version(chart_yaml_path)
|
chart_version = get_chart_version(chart_yaml_path)
|
||||||
gradle_version = get_gradle_version(gradle_path)
|
gradle_version = get_gradle_version(gradle_path)
|
||||||
|
|
||||||
if chart_version["app"] != gradle_version:
|
if chart_version != gradle_version:
|
||||||
new_chart_version = get_new_chart_version(chart_version["chart"], chart_version["app"], gradle_version, )
|
|
||||||
print(
|
print(
|
||||||
f"Versions do not match. Updating Chart.yaml from {chart_version['chart']} to {new_chart_version}."
|
f"Versions do not match. Updating Chart.yaml from {chart_version} to {gradle_version}."
|
||||||
)
|
)
|
||||||
update_chart_version(chart_yaml_path, new_chart_version, gradle_version)
|
update_chart_version(chart_yaml_path, gradle_version)
|
||||||
else:
|
else:
|
||||||
print("Versions match. No update required.")
|
print("Versions match. No update required.")
|
||||||
|
|||||||
47
.github/workflows/lint-helm-charts.yml
vendored
47
.github/workflows/lint-helm-charts.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: Lint and Test Helm Charts
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint-test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Helm
|
|
||||||
uses: azure/setup-helm@v4
|
|
||||||
|
|
||||||
- name: Set up python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.10'
|
|
||||||
|
|
||||||
- name: Run pre-commit
|
|
||||||
uses: pre-commit/action@v3.0.1
|
|
||||||
with:
|
|
||||||
extra_args: helm-docs-built
|
|
||||||
|
|
||||||
- name: Set up chart-testing
|
|
||||||
uses: helm/chart-testing-action@v2
|
|
||||||
|
|
||||||
- name: Run chart-testing (list-changed)
|
|
||||||
id: list-changed
|
|
||||||
run: |
|
|
||||||
changed=$(ct list-changed --target-branch ${{ github.event.repository.default_branch }})
|
|
||||||
if [[ -n "$changed" ]]; then
|
|
||||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run chart-testing
|
|
||||||
if: steps.list-changed.outputs.changed == 'true'
|
|
||||||
run: ct lint --target-branch ${{ github.event.repository.default_branch }} --validate-maintainers=false
|
|
||||||
31
.github/workflows/release-helm-charts.yml
vendored
31
.github/workflows/release-helm-charts.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: Release Helm charts
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up git config
|
|
||||||
run: |
|
|
||||||
git config --global user.name "github-actions[bot]"
|
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Run chart-releaser
|
|
||||||
uses: helm/chart-releaser-action@v1.6.0
|
|
||||||
with:
|
|
||||||
config: "./cr.yaml"
|
|
||||||
charts_dir: "chart"
|
|
||||||
env:
|
|
||||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
4
.github/workflows/sync_files.yml
vendored
4
.github/workflows/sync_files.yml
vendored
@@ -26,10 +26,6 @@ jobs:
|
|||||||
run: pip install pyyaml
|
run: pip install pyyaml
|
||||||
- name: Sync versions
|
- name: Sync versions
|
||||||
run: python .github/scripts/gradle_to_chart.py
|
run: python .github/scripts/gradle_to_chart.py
|
||||||
- name: Run pre-commit helm-docs-built
|
|
||||||
uses: pre-commit/action@v3.0.1
|
|
||||||
with:
|
|
||||||
extra_args: helm-docs-built
|
|
||||||
- name: Set up git config
|
- name: Set up git config
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.name "github-actions[bot]"
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,7 +4,6 @@ bin/
|
|||||||
tmp/
|
tmp/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.bak
|
*.bak
|
||||||
*.exe
|
|
||||||
*.swp
|
*.swp
|
||||||
*~.nib
|
*~.nib
|
||||||
local.properties
|
local.properties
|
||||||
@@ -111,6 +110,7 @@ watchedFolders/
|
|||||||
*.war
|
*.war
|
||||||
*.nar
|
*.nar
|
||||||
*.ear
|
*.ear
|
||||||
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.rar
|
*.rar
|
||||||
*.db
|
*.db
|
||||||
|
|||||||
@@ -37,9 +37,3 @@ repos:
|
|||||||
language: python
|
language: python
|
||||||
exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
|
exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
|
||||||
files: ^.*(\.html|\.css|\.js)$
|
files: ^.*(\.html|\.css|\.js)$
|
||||||
- repo: https://github.com/norwoodj/helm-docs
|
|
||||||
rev: v1.14.2
|
|
||||||
hooks:
|
|
||||||
- id: helm-docs-built
|
|
||||||
args:
|
|
||||||
- --chart-search-root=chart
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ If you would like to add or modify a translation, please see [How to add new lan
|
|||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
|
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
|
||||||
|
|
||||||
## Fixing Bugs or Adding a New Feature
|
## Fixing Bugs or Adding a New Feature
|
||||||
|
|
||||||
@@ -41,4 +41,4 @@ If, at any point of time, you have a question, please feel free to ask in the sa
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).
|
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# New Database Backup and Import Functionality
|
# New Database Backup and Import Functionality
|
||||||
|
|
||||||
> [!IMPORTANT]
|
**Full activation will take place on approximately January 5th, 2025!**
|
||||||
> **Full activation will take place on approximately January 5th, 2025!**
|
|
||||||
|
|
||||||
Why is the waiting time six months?
|
Why is the waiting time six months?
|
||||||
|
|
||||||
|
|||||||
@@ -1,575 +0,0 @@
|
|||||||
# Stirling-PDF Developer Guide
|
|
||||||
|
|
||||||
## 1. Introduction
|
|
||||||
|
|
||||||
Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
|
|
||||||
|
|
||||||
## 2. Project Overview
|
|
||||||
|
|
||||||
Stirling-PDF is built using:
|
|
||||||
|
|
||||||
- Spring Boot + Thymeleaf
|
|
||||||
- PDFBox
|
|
||||||
- LibreOffice
|
|
||||||
- OcrMyPdf
|
|
||||||
- HTML, CSS, JavaScript
|
|
||||||
- Docker
|
|
||||||
- PDF.js
|
|
||||||
- PDF-LIB.js
|
|
||||||
- Lombok
|
|
||||||
|
|
||||||
## 3. Development Environment Setup
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- Git
|
|
||||||
- Java JDK 17 or later
|
|
||||||
- Gradle 7.0 or later (Included within repo)
|
|
||||||
|
|
||||||
### Setup Steps
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
|
||||||
cd Stirling-PDF
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install Docker and JDK17 if not already installed.
|
|
||||||
|
|
||||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ or VSCode
|
|
||||||
|
|
||||||
4. Lombok Setup
|
|
||||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
|
||||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
|
||||||
|
|
||||||
5. Add environment variable
|
|
||||||
For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
|
|
||||||
|
|
||||||
## 4. Project Structure
|
|
||||||
|
|
||||||
```bash
|
|
||||||
Stirling-PDF/
|
|
||||||
├── .github/ # GitHub-specific files (workflows, issue templates)
|
|
||||||
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
|
||||||
├── cucumber/ # Cucumber test files
|
|
||||||
│ ├── features/
|
|
||||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
|
||||||
├── docs/ # Documentation files
|
|
||||||
├── exampleYmlFiles/ # Example YAML configuration files
|
|
||||||
├── images/ # Image assets
|
|
||||||
├── pipeline/ # Pipeline-related files (generated at runtime)
|
|
||||||
├── scripts/ # Utility scripts
|
|
||||||
├── src/ # Source code
|
|
||||||
│ ├── main/
|
|
||||||
│ │ ├── java/
|
|
||||||
│ │ │ └── stirling/
|
|
||||||
│ │ │ └── software/
|
|
||||||
│ │ │ └── SPDF/
|
|
||||||
│ │ │ ├── config/
|
|
||||||
│ │ │ ├── controller/
|
|
||||||
│ │ │ ├── model/
|
|
||||||
│ │ │ ├── repository/
|
|
||||||
│ │ │ ├── service/
|
|
||||||
│ │ │ └── utils/
|
|
||||||
│ │ └── resources/
|
|
||||||
│ │ ├── static/
|
|
||||||
│ │ │ ├── css/
|
|
||||||
│ │ │ ├── js/
|
|
||||||
│ │ │ └── pdfjs/
|
|
||||||
│ │ └── templates/
|
|
||||||
│ └── test/
|
|
||||||
│ └── java/
|
|
||||||
│ └── stirling/
|
|
||||||
│ └── software/
|
|
||||||
│ └── SPDF/
|
|
||||||
├── build.gradle # Gradle build configuration
|
|
||||||
├── Dockerfile # Main Dockerfile
|
|
||||||
├── Dockerfile-ultra-lite # Dockerfile for ultra-lite version
|
|
||||||
├── Dockerfile-fat # Dockerfile for fat version
|
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
|
||||||
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Docker-based Development
|
|
||||||
|
|
||||||
Stirling-PDF offers several Docker versions:
|
|
||||||
|
|
||||||
- Full: All features included
|
|
||||||
- Ultra-Lite: Basic PDF operations only
|
|
||||||
- Fat: Includes additional libraries and fonts predownloaded
|
|
||||||
|
|
||||||
### Example Docker Compose Files
|
|
||||||
|
|
||||||
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory such as :
|
|
||||||
|
|
||||||
- `docker-compose-latest.yml`: Latest version without security features
|
|
||||||
- `docker-compose-latest-security.yml`: Latest version with security features enabled
|
|
||||||
- `docker-compose-latest-fat-security.yml`: Fat version with security features enabled
|
|
||||||
|
|
||||||
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
stirling-pdf:
|
|
||||||
container_name: Stirling-PDF-Security
|
|
||||||
image: frooodle/s-pdf:latest
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 4G
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 16
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
|
||||||
- /stirling/latest/config:/configs:rw
|
|
||||||
- /stirling/latest/logs:/logs:rw
|
|
||||||
environment:
|
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
|
||||||
SECURITY_ENABLELOGIN: "true"
|
|
||||||
PUID: 1002
|
|
||||||
PGID: 1002
|
|
||||||
UMASK: "022"
|
|
||||||
SYSTEM_DEFAULTLOCALE: en-US
|
|
||||||
UI_APPNAME: Stirling-PDF
|
|
||||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
|
||||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
|
||||||
SYSTEM_MAXFILESIZE: "100"
|
|
||||||
METRICS_ENABLED: "true"
|
|
||||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
|
||||||
restart: on-failure:5
|
|
||||||
```
|
|
||||||
|
|
||||||
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building Docker Images
|
|
||||||
|
|
||||||
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
|
||||||
|
|
||||||
1. Set the security environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export DOCKER_ENABLE_SECURITY=false # or true for security-enabled builds
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Build the project with Gradle:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew clean build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Build the Docker images:
|
|
||||||
|
|
||||||
For the latest version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
For the ultra-lite version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
|
||||||
```
|
|
||||||
|
|
||||||
For the fat version (with security enabled):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export DOCKER_ENABLE_SECURITY=true
|
|
||||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
|
||||||
|
|
||||||
## 6. Testing
|
|
||||||
|
|
||||||
### Comprehensive Testing Script
|
|
||||||
|
|
||||||
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
|
||||||
|
|
||||||
To run the test script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script performs the following actions:
|
|
||||||
|
|
||||||
1. Builds all Docker images (full, ultra-lite, fat)
|
|
||||||
2. Runs each version to ensure it starts correctly
|
|
||||||
3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
|
|
||||||
|
|
||||||
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
|
||||||
|
|
||||||
### Full Testing with Docker
|
|
||||||
|
|
||||||
1. Build and run the Docker container per the above instructions:
|
|
||||||
|
|
||||||
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
|
||||||
|
|
||||||
### Local Testing (Java and UI Components)
|
|
||||||
|
|
||||||
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
|
||||||
|
|
||||||
- Java backend logic
|
|
||||||
- RESTful API endpoints
|
|
||||||
- JavaScript functionality
|
|
||||||
- User interface components and styling
|
|
||||||
- Thymeleaf templates
|
|
||||||
|
|
||||||
To run Stirling-PDF locally:
|
|
||||||
|
|
||||||
1. Compile and run the project using built in IDE methods or by running:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./gradlew bootRun
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Access the application at `http://localhost:8080` in your web browser.
|
|
||||||
|
|
||||||
3. Manually test the features you're working on through the UI.
|
|
||||||
|
|
||||||
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
|
||||||
|
|
||||||
Important notes:
|
|
||||||
|
|
||||||
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
|
|
||||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
|
||||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
|
||||||
|
|
||||||
## 7. Contributing
|
|
||||||
|
|
||||||
1. Fork the repository on GitHub.
|
|
||||||
2. Create a new branch for your feature or bug fix.
|
|
||||||
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
|
||||||
4. Test your changes thoroughly in the Docker environment.
|
|
||||||
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./test.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Push your changes to your fork.
|
|
||||||
7. Submit a pull request to the main repository.
|
|
||||||
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
|
|
||||||
|
|
||||||
When you raise a PR:
|
|
||||||
|
|
||||||
- The `test.sh` script will run automatically against your PR.
|
|
||||||
- The PR checks will verify versioning and dependency updates.
|
|
||||||
- Documentation will be automatically updated for dependency changes.
|
|
||||||
- Security issues will be checked using Snyk and PixeeBot.
|
|
||||||
|
|
||||||
Address any issues that arise from these checks before finalizing your pull request.
|
|
||||||
|
|
||||||
## 8. API Documentation
|
|
||||||
|
|
||||||
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
|
||||||
|
|
||||||
## 9. Customization
|
|
||||||
|
|
||||||
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
|
||||||
|
|
||||||
- Application name and branding
|
|
||||||
- Security settings
|
|
||||||
- UI customization
|
|
||||||
- Endpoint management
|
|
||||||
|
|
||||||
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
|
||||||
```
|
|
||||||
|
|
||||||
Refer to the main README for a full list of customization options.
|
|
||||||
|
|
||||||
## 10. Language Translations
|
|
||||||
|
|
||||||
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
/scripts/replace_translation_line.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This script helps you make consistent replacements across language files.
|
|
||||||
|
|
||||||
When contributing translations:
|
|
||||||
|
|
||||||
1. Use the helper script for multi-file changes.
|
|
||||||
2. Ensure all language files are updated consistently.
|
|
||||||
3. The PR checks will verify consistency in language file updates.
|
|
||||||
|
|
||||||
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
|
||||||
|
|
||||||
## Code examples
|
|
||||||
|
|
||||||
### Overview of Thymeleaf
|
|
||||||
|
|
||||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot
|
|
||||||
|
|
||||||
### Thymeleaf overview
|
|
||||||
|
|
||||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
|
||||||
|
|
||||||
Some examples of this are:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
||||||
or
|
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
```
|
|
||||||
|
|
||||||
Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside in generating the html, and block being the actual element type.
|
|
||||||
In this case we are inserting the ``navbar`` entry within the ``fragments/navbar.html`` fragment into the ``th:block`` element.
|
|
||||||
|
|
||||||
They can be more complex such as:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
|
||||||
```
|
|
||||||
|
|
||||||
Which is the same as above but passes the parameters title and header into the fragment common.html to be used in its HTML generation
|
|
||||||
|
|
||||||
Thymeleaf can also be used to loop through objects or pass things from java side into html side.
|
|
||||||
|
|
||||||
```java
|
|
||||||
@GetMapping
|
|
||||||
public String newFeaturePage(Model model) {
|
|
||||||
model.addAttribute("exampleData", exampleData);
|
|
||||||
return "new-feature";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
in above example if exampleData is a list of plain java objects of class Person and within it you had id, name, age etc. You can reference it like so
|
|
||||||
|
|
||||||
```html
|
|
||||||
<tbody>
|
|
||||||
<!-- Use th:each to iterate over the list -->
|
|
||||||
<tr th:each="person : ${exampleData}">
|
|
||||||
<td th:text="${person.id}"></td>
|
|
||||||
<td th:text="${person.name}"></td>
|
|
||||||
<td th:text="${person.age}"></td>
|
|
||||||
<td th:text="${person.email}"></td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
```
|
|
||||||
|
|
||||||
This would generate n entries of tr for each person in exampleData
|
|
||||||
|
|
||||||
### Adding a New Feature to the Backend (API)
|
|
||||||
|
|
||||||
1. **Create a New Controller:**
|
|
||||||
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/api` directory.
|
|
||||||
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
|
||||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
|
||||||
|
|
||||||
```java
|
|
||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/new-feature")
|
|
||||||
@Tag(name = "General", description = "General APIs")
|
|
||||||
public class NewFeatureController {
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
|
||||||
public String newFeature() {
|
|
||||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Define the Service Layer:** (Not required but often useful)
|
|
||||||
- Create a new service class in the `src/main/java/stirling/software/SPDF/service` directory.
|
|
||||||
- Implement the business logic for the new feature.
|
|
||||||
|
|
||||||
```java
|
|
||||||
package stirling.software.SPDF.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class NewFeatureService {
|
|
||||||
|
|
||||||
public String getNewFeatureData() {
|
|
||||||
// Implement business logic here
|
|
||||||
return "New Feature Data";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2b. **Integrate the Service with the Controller:**
|
|
||||||
|
|
||||||
- Autowire the service class in the controller and use it to handle the API request.
|
|
||||||
|
|
||||||
```java
|
|
||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import stirling.software.SPDF.service.NewFeatureService;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/new-feature")
|
|
||||||
@Tag(name = "General", description = "General APIs")
|
|
||||||
public class NewFeatureController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private NewFeatureService newFeatureService;
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
|
||||||
public String newFeature() {
|
|
||||||
return newFeatureService.getNewFeatureData();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding a New Feature to the Frontend (UI)
|
|
||||||
|
|
||||||
1. **Create a New Thymeleaf Template:**
|
|
||||||
- Create a new HTML file in the `src/main/resources/templates` directory.
|
|
||||||
- Use Thymeleaf attributes to dynamically generate content.
|
|
||||||
- Use `extract-page.html` as a base example for the HTML template, useful to ensure importing of the general layout, navbar and footer.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
|
||||||
<head>
|
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="page-container">
|
|
||||||
<div id="content-wrap">
|
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
|
||||||
<br><br>
|
|
||||||
<div class="container">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6 bg-card">
|
|
||||||
<div class="tool-header">
|
|
||||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
|
||||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
|
||||||
</div>
|
|
||||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
|
||||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
|
||||||
<input type="hidden" id="customMode" name="customMode" value="">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
|
||||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Create a New Controller for the UI:**
|
|
||||||
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/ui` directory.
|
|
||||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
|
||||||
|
|
||||||
```java
|
|
||||||
package stirling.software.SPDF.controller.ui;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.ui.Model;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import stirling.software.SPDF.service.NewFeatureService;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("/new-feature")
|
|
||||||
public class NewFeatureUIController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private NewFeatureService newFeatureService;
|
|
||||||
|
|
||||||
@GetMapping
|
|
||||||
public String newFeaturePage(Model model) {
|
|
||||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
|
||||||
return "new-feature";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Update the Navigation Bar:**
|
|
||||||
- Add a link to the new feature page in the navigation bar.
|
|
||||||
- Update the `src/main/resources/templates/fragments/navbar.html` file.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" th:href="@{/new-feature}">New Feature</a>
|
|
||||||
</li>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
|
||||||
|
|
||||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
|
||||||
|
|
||||||
### 1. Locate Existing Language Files
|
|
||||||
|
|
||||||
Find the existing `messages.properties` files in the `src/main/resources` directory. You'll see files like:
|
|
||||||
|
|
||||||
- `messages.properties` (default, usually English)
|
|
||||||
- `messages_en_GB.properties`
|
|
||||||
- `messages_fr_FR.properties`
|
|
||||||
- `messages_de_DE.properties`
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
### 2. Add New Translation Entries
|
|
||||||
|
|
||||||
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
|
||||||
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
|
||||||
you might add:
|
|
||||||
|
|
||||||
```properties
|
|
||||||
pdfSplitter.title=PDF Splitter
|
|
||||||
pdfSplitter.description=Split your PDF into multiple documents
|
|
||||||
pdfSplitter.button.split=Split PDF
|
|
||||||
pdfSplitter.input.pages=Enter page numbers to split
|
|
||||||
```
|
|
||||||
|
|
||||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
|
||||||
|
|
||||||
### 3. Use Translations in Thymeleaf Templates
|
|
||||||
|
|
||||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
|
||||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
|
||||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
|
||||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
|
||||||
@@ -15,7 +15,6 @@ ENV DOCKER_ENABLE_SECURITY=false \
|
|||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
|
||||||
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
|
||||||
COPY scripts/installFonts.sh /scripts/installFonts.sh
|
|
||||||
COPY pipeline /pipeline
|
COPY pipeline /pipeline
|
||||||
COPY build/libs/*.jar app.jar
|
COPY build/libs/*.jar app.jar
|
||||||
|
|
||||||
@@ -34,11 +33,11 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
su-exec \
|
su-exec \
|
||||||
openjdk21-jre && \
|
openjdk21-jre && \
|
||||||
# User permissions
|
# User permissions
|
||||||
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \
|
mkdir /configs /logs /customFiles && \
|
||||||
chmod +x /scripts/*.sh && \
|
chmod +x /scripts/*.sh && \
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
|
|
||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
## User Guide for Local Directory Scanning and File Processing
|
## User Guide for Local Directory Scanning and File Processing
|
||||||
|
|
||||||
### Setting Up Watched Folders
|
### Setting Up Watched Folders:
|
||||||
|
|
||||||
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
|
||||||
- The default directory for this is `./pipeline/watchedFolders/`.
|
- The default directory for this is `./pipeline/watchedFolders/`
|
||||||
- Place any directories you want to be scanned into this folder. This folder should contain multiple folders, each for their own tasks and pipelines.
|
- Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.
|
||||||
|
|
||||||
### Configuring Processing with JSON Files
|
### Configuring Processing with JSON Files:
|
||||||
|
- In each directory you want processed (e.g `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
|
||||||
- In each directory you want processed (e.g., `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
|
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.r
|
||||||
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them). This can be made, configured, and downloaded from the Stirling-PDF Pipeline interface.
|
|
||||||
|
|
||||||
### Automatic Scanning and Processing
|
|
||||||
|
|
||||||
|
### Automatic Scanning and Processing:
|
||||||
- The system automatically checks the watched folder every minute for new directories and files to process.
|
- The system automatically checks the watched folder every minute for new directories and files to process.
|
||||||
- When a directory with a valid JSON configuration file is found, it begins processing the files inside according to the configuration.
|
- When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.
|
||||||
|
|
||||||
### Processing Steps
|
|
||||||
|
|
||||||
|
### Processing Steps:
|
||||||
- Files in each directory are processed according to the instructions in the JSON file.
|
- Files in each directory are processed according to the instructions in the JSON file.
|
||||||
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to the next process.
|
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.
|
||||||
|
|
||||||
### Results and Output
|
|
||||||
|
|
||||||
|
### Results and Output:
|
||||||
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
|
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
|
||||||
- Each processed file is named and organized according to the rules set in the JSON configuration.
|
- Each processed file is named and organized according to the rules set in the JSON configuration.
|
||||||
|
|
||||||
### Completion and Cleanup
|
### Completion and Cleanup:
|
||||||
|
|
||||||
- Once processing is complete, the original files in the watched folder's directory are removed.
|
- Once processing is complete, the original files in the watched folder's directory are removed.
|
||||||
- You can find the processed files in the designated output location.
|
- You can find the processed files in the designated output location.
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling:
|
||||||
|
|
||||||
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
|
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
|
||||||
|
|
||||||
### User Interaction
|
### User Interaction:
|
||||||
|
|
||||||
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
|
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
|
||||||
- The system handles the rest, including scanning, processing, and outputting results.
|
- The system handles the rest, including scanning, processing, and outputting results.
|
||||||
|
|||||||
@@ -1,47 +1,43 @@
|
|||||||
<p align="center">
|
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
|
||||||
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80">
|
|
||||||
<br>
|
|
||||||
<h1 align="center">Stirling-PDF</h1>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# How to add new languages to Stirling-PDF
|
# How to add new languages to Stirling-PDF
|
||||||
|
|
||||||
Fork Stirling-PDF and create a new branch out of `main`.
|
Fork Stirling-PDF and make a new branch out of Main
|
||||||
|
|
||||||
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
|
Then add reference to the language in the navbar by adding a new language entry to the dropdown
|
||||||
|
|
||||||
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
|
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
|
||||||
- Add a flag SVG file to: [flags directory](https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags)
|
and add a flag svg file to
|
||||||
|
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags
|
||||||
|
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
|
||||||
|
If your language isn't represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
|
||||||
|
|
||||||
Any SVG flags are fine; most of the current ones were sourced from [here](https://flagicons.lipis.dev/). If your language isn't represented by a flag, choose a similar one, such as Saudi Arabia's flag for Arabic.
|
For example to add Polish you would add
|
||||||
|
|
||||||
For example, to add Polish, you would add:
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL">
|
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
|
||||||
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
|
||||||
</a>
|
</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `data-bs-language-code` is the code used to reference the file in the next step.
|
The data-language-code is the code used to reference the file in the next step.
|
||||||
|
|
||||||
### Add Language Property File
|
Start by copying the existing english property file
|
||||||
|
|
||||||
Start by copying the existing English property file:
|
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
||||||
|
|
||||||
- [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
|
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties
|
||||||
|
|
||||||
Copy and rename it to `messages_{your data-bs-language-code here}.properties`. In the Polish example, you would set the name to `messages_pl_PL.properties`.
|
Then simply translate all property entries within that file and make a PR into main for others to use!
|
||||||
|
|
||||||
Then simply translate all property entries within that file and make a Pull Request (PR) into `main` for others to use!
|
If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but won't be able to verify the translations themselves)
|
||||||
|
|
||||||
If you do not have a Java IDE, I am happy to verify that the changes work once you raise the PR (but I won't be able to verify the translations themselves).
|
|
||||||
|
|
||||||
## Handling Untranslatable Strings
|
## Handling Untranslatable Strings
|
||||||
|
|
||||||
Sometimes, certain strings in the properties file may not require translation because they are the same in the target language or are universal (like names of protocols, certain terminologies, etc.). To ensure accurate statistics for language progress, these strings should be added to the `ignore_translation.toml` file located in the `scripts` directory. This will exclude them from the translation progress calculations.
|
Sometimes, certain strings in the properties file may not require translation because they are the same in the target language or are universal (like names of protocols, certain terminologies, etc.). To ensure accurate statistics for language progress, these strings should be added to the `ignore_translation.toml` file located in the `scripts` directory. This will exclude them from the translation progress calculations.
|
||||||
|
|
||||||
For example, if the English string `error=Error` does not need translation in Polish, add it to the `ignore_translation.toml` under the Polish section:
|
For example, if the English string error=Error does not need translation in Polish, add it to the ignore_translation.toml under the Polish section:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[pl_PL]
|
[pl_PL]
|
||||||
@@ -53,9 +49,7 @@ ignore = [
|
|||||||
|
|
||||||
## Add New Translation Tags
|
## Add New Translation Tags
|
||||||
|
|
||||||
> [!IMPORTANT]
|
- **Important**: If you add any new translation tags, they must first be added to the `messages_en_GB.properties` file. This ensures consistency across all language files.
|
||||||
> If you add any new translation tags, they must first be added to the `messages_en_GB.properties` file. This ensures consistency across all language files.
|
|
||||||
|
|
||||||
- New translation tags **must be added** to the `messages_en_GB.properties` file to maintain a reference for other languages.
|
- New translation tags **must be added** to the `messages_en_GB.properties` file to maintain a reference for other languages.
|
||||||
- After adding the new tags to `messages_en_GB.properties`, add and translate them in the respective language file (e.g., `messages_pl_PL.properties`).
|
- After adding the new tags to `messages_en_GB.properties`, add and translate them in the respective language file (e.g., `messages_pl_PL.properties`).
|
||||||
|
|
||||||
|
|||||||
@@ -3,37 +3,35 @@
|
|||||||
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
|
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
|
||||||
|
|
||||||
## My OCR used to work and now doesn't!
|
## My OCR used to work and now doesn't!
|
||||||
|
The paths have changed for the tessadata locations on new docker images, please use ``/usr/share/tessdata`` (Others should still work for backwards compatibility but might not)
|
||||||
The paths have changed for the tessdata locations on new Docker images. Please use `/usr/share/tessdata` (Others should still work for backward compatibility but might not).
|
|
||||||
|
|
||||||
## How does the OCR Work
|
## How does the OCR Work
|
||||||
|
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
|
||||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF), which in turn uses Tesseract for its text recognition. All credit goes to them for this awesome work!
|
All credit goes to them for this awesome work!
|
||||||
|
|
||||||
## Language Packs
|
## Language Packs
|
||||||
|
|
||||||
Tesseract OCR supports a variety of languages. You can find additional language packs in the Tesseract GitHub repositories:
|
Tesseract OCR supports a variety of languages. You can find additional language packs in the Tesseract GitHub repositories:
|
||||||
|
|
||||||
- [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast): These language packs are smaller and faster to load but may provide lower recognition accuracy.
|
- [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast): These language packs are smaller and faster to load, but may provide lower recognition accuracy.
|
||||||
- [tessdata](https://github.com/tesseract-ocr/tessdata): These language packs are larger and provide better recognition accuracy, but may take longer to load.
|
- [tessdata](https://github.com/tesseract-ocr/tessdata): These language packs are larger and provide better recognition accuracy, but may take longer to load.
|
||||||
|
|
||||||
Depending on your requirements, you can choose the appropriate language pack for your use case. By default, Stirling-PDF uses `tessdata_fast` for English, but this can be replaced.
|
Depending on your requirements, you can choose the appropriate language pack for your use case. By default Stirling-PDF uses the tessdata_fast eng but this can be replaced.
|
||||||
|
|
||||||
### Installing Language Packs
|
### Installing Language Packs
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
||||||
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||||
|
|
||||||
**DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.**
|
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
|
||||||
|
|
||||||
### Docker Setup
|
#### Docker
|
||||||
|
|
||||||
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
|
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
|
||||||
|
|
||||||
#### Docker Compose
|
#### Docker Compose
|
||||||
|
|
||||||
Modify your `docker-compose.yml` file to include the following volume configuration:
|
Modify your `docker-compose.yml` file to include the following volume configuration:
|
||||||
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
your_service_name:
|
your_service_name:
|
||||||
@@ -42,19 +40,18 @@ services:
|
|||||||
- /location/of/trainingData:/usr/share/tessdata
|
- /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker Run
|
|
||||||
|
|
||||||
Add the following to your existing Docker run command:
|
|
||||||
|
|
||||||
|
#### Docker run
|
||||||
|
Add the following to your existing docker run command
|
||||||
```bash
|
```bash
|
||||||
-v /location/of/trainingData:/usr/share/tessdata
|
-v /location/of/trainingData:/usr/share/tessdata
|
||||||
```
|
```
|
||||||
|
|
||||||
### Non-Docker Setup
|
#### Non-Docker
|
||||||
|
If you are not using Docker, you need to install the OCR components, including the ocrmypdf app.
|
||||||
|
You can see [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html)
|
||||||
|
|
||||||
If you are not using Docker, you need to install the OCR components, including the `ocrmypdf` app. You can see the [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html).
|
Debian based systems, install languages with this command:
|
||||||
|
|
||||||
For Debian-based systems, install languages with this command:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update &&\
|
sudo apt update &&\
|
||||||
@@ -68,7 +65,7 @@ apt search tesseract-ocr-
|
|||||||
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
|
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora:
|
Fedora:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All languages
|
# All languages
|
||||||
|
|||||||
149
LocalRunGuide.md
149
LocalRunGuide.md
@@ -1,35 +1,48 @@
|
|||||||
|
|
||||||
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
|
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
|
||||||
|
|
||||||
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
|
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
|
||||||
|
|
||||||
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
|
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
|
||||||
|
|
||||||
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. The amount of dependencies is to actually reduce overall size, i.e., installing LibreOffice subcomponents rather than the full LibreOffice package.
|
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps.
|
||||||
|
The amount of dependencies is to actually reduce overall size, ie installing LibreOffice sub components rather than full LibreOffice package.
|
||||||
|
|
||||||
You could theoretically use a Distrobox/Toolbox if your distribution has old or not all packages. But you might just as well use the Docker container then.
|
You could theoretically use a Distrobox/Toolbox, if your Distribution has old or not all Packages. But you might just as well use the Docker Container then.
|
||||||
|
|
||||||
### Step 1: Prerequisites
|
### Step 1: Prerequisites
|
||||||
|
|
||||||
Install the following software, if not already installed:
|
Install the following software, if not already installed:
|
||||||
|
|
||||||
- Java 17 or later (21 recommended)
|
- Java 17 or later (21 recommended)
|
||||||
|
|
||||||
- Gradle 7.0 or later (included within repo so not needed on server)
|
- Gradle 7.0 or later (included within repo so not needed on server)
|
||||||
|
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
- Python 3.8 (with pip)
|
- Python 3.8 (with pip)
|
||||||
|
|
||||||
- Make
|
- Make
|
||||||
|
|
||||||
- GCC/G++
|
- GCC/G++
|
||||||
|
|
||||||
- Automake
|
- Automake
|
||||||
|
|
||||||
- Autoconf
|
- Autoconf
|
||||||
|
|
||||||
- libtool
|
- libtool
|
||||||
|
|
||||||
- pkg-config
|
- pkg-config
|
||||||
|
|
||||||
- zlib1g-dev
|
- zlib1g-dev
|
||||||
|
|
||||||
- libleptonica-dev
|
- libleptonica-dev
|
||||||
|
|
||||||
For Debian-based systems, you can use the following command:
|
For Debian-based systems, you can use the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
|
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora-based systems use this command:
|
For Fedora-based systems use this command:
|
||||||
@@ -39,7 +52,6 @@ sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zli
|
|||||||
```
|
```
|
||||||
|
|
||||||
For non-root users with Nix Package Manager, use the following command:
|
For non-root users with Nix Package Manager, use the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-channel --update
|
nix-channel --update
|
||||||
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
|
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
|
||||||
@@ -51,37 +63,45 @@ For Debian and Fedora, you can build it from source using the following commands
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir ~/.git
|
mkdir ~/.git
|
||||||
cd ~/.git && \
|
cd ~/.git &&\
|
||||||
git clone https://github.com/agl/jbig2enc.git && \
|
git clone https://github.com/agl/jbig2enc.git &&\
|
||||||
cd jbig2enc && \
|
cd jbig2enc &&\
|
||||||
./autogen.sh && \
|
./autogen.sh &&\
|
||||||
./configure && \
|
./configure &&\
|
||||||
make && \
|
make &&\
|
||||||
sudo make install
|
sudo make install
|
||||||
```
|
```
|
||||||
|
|
||||||
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
|
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-env -iA nixpkgs.jbig2enc
|
nix-env -iA nixpkgs.jbig2enc
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 3: Install Additional Software
|
### Step 3: Install Additional Software
|
||||||
|
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality.
|
||||||
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and OpenCV for pattern recognition functionality.
|
|
||||||
|
|
||||||
Install the following software:
|
Install the following software:
|
||||||
|
|
||||||
- libreoffice-core
|
- libreoffice-core
|
||||||
|
|
||||||
- libreoffice-common
|
- libreoffice-common
|
||||||
|
|
||||||
- libreoffice-writer
|
- libreoffice-writer
|
||||||
|
|
||||||
- libreoffice-calc
|
- libreoffice-calc
|
||||||
|
|
||||||
- libreoffice-impress
|
- libreoffice-impress
|
||||||
|
|
||||||
- python3-uno
|
- python3-uno
|
||||||
|
|
||||||
- unoconv
|
- unoconv
|
||||||
|
|
||||||
- pngquant
|
- pngquant
|
||||||
|
|
||||||
- unpaper
|
- unpaper
|
||||||
|
|
||||||
- ocrmypdf
|
- ocrmypdf
|
||||||
|
|
||||||
- opencv-python-headless
|
- opencv-python-headless
|
||||||
|
|
||||||
For Debian-based systems, you can use the following command:
|
For Debian-based systems, you can use the following command:
|
||||||
@@ -108,52 +128,51 @@ pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
|||||||
### Step 4: Clone and Build Stirling-PDF
|
### Step 4: Clone and Build Stirling-PDF
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/.git && \
|
cd ~/.git &&\
|
||||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && \
|
git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\
|
||||||
cd Stirling-PDF && \
|
cd Stirling-PDF &&\
|
||||||
chmod +x ./gradlew && \
|
chmod +x ./gradlew &&\
|
||||||
./gradlew build
|
./gradlew build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 5: Move Jar to Desired Location
|
### Step 5: Move jar to desired location
|
||||||
|
|
||||||
After the build process, a `.jar` file will be generated in the `build/libs` directory. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. This folder is required for the Python scripts using OpenCV.
|
After the build process, a `.jar` file will be generated in the `build/libs` directory.
|
||||||
|
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
|
||||||
|
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
|
||||||
|
This folder is required for the python scripts using OpenCV.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo mkdir /opt/Stirling-PDF && \
|
sudo mkdir /opt/Stirling-PDF &&\
|
||||||
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ && \
|
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
|
||||||
sudo mv scripts /opt/Stirling-PDF/ && \
|
sudo mv scripts /opt/Stirling-PDF/ &&\
|
||||||
echo "Scripts installed."
|
echo "Scripts installed."
|
||||||
```
|
```
|
||||||
|
|
||||||
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
|
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
|
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 6: Other Files
|
### Step 6: Other files
|
||||||
|
|
||||||
#### OCR
|
#### OCR
|
||||||
|
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
|
||||||
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-English scanning.
|
|
||||||
|
|
||||||
##### Installing Language Packs
|
##### Installing Language Packs
|
||||||
|
Easiest is to use the langpacks provided by your repositories. Skip the other steps.
|
||||||
|
|
||||||
The easiest method is to use the language packs provided by your repositories. Skip the other steps if they are available.
|
Manual:
|
||||||
|
|
||||||
**Manual:**
|
|
||||||
|
|
||||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
||||||
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||||
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
||||||
|
|
||||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
||||||
|
|
||||||
**Debian-based systems**, install languages with this command:
|
Debian based systems, install languages with this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt update && \
|
sudo apt update &&\
|
||||||
# All languages
|
# All languages
|
||||||
# sudo apt install -y 'tesseract-ocr-*'
|
# sudo apt install -y 'tesseract-ocr-*'
|
||||||
|
|
||||||
@@ -164,7 +183,7 @@ apt search tesseract-ocr-
|
|||||||
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
|
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Fedora:**
|
Fedora:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All languages
|
# All languages
|
||||||
@@ -177,13 +196,13 @@ dnf search -C tesseract-langpack-
|
|||||||
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
||||||
```
|
```
|
||||||
|
|
||||||
**Nix:**
|
Nix:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-env -iA nixpkgs.tesseract
|
nix-env -iA nixpkgs.tesseract
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Nix Package Manager pre-installs almost all the language packs when Tesseract is installed.
|
**Note:** Nix Package Manager pre-installs almost all the language packs when tesseract is installed.
|
||||||
|
|
||||||
### Step 7: Run Stirling-PDF
|
### Step 7: Run Stirling-PDF
|
||||||
|
|
||||||
@@ -195,13 +214,11 @@ or
|
|||||||
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Since LibreOffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
|
Since libreoffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
|
||||||
|
|
||||||
```
|
```
|
||||||
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory ‘/run/user/1501’: Permission denied
|
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory ‘/run/user/1501’: Permission denied
|
||||||
```
|
```
|
||||||
|
To resolve this, before starting the Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
|
||||||
To resolve this, before starting Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir temp
|
mkdir temp
|
||||||
@@ -211,10 +228,9 @@ or
|
|||||||
java -jar ./Stirling-PDF-*.jar
|
java -jar ./Stirling-PDF-*.jar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 8: Adding a Desktop Icon
|
### Step 8: Adding a Desktop icon
|
||||||
|
|
||||||
This will add a modified app starter to your app menu.
|
|
||||||
|
|
||||||
|
This will add a modified Appstarter to your Appmenu.
|
||||||
```bash
|
```bash
|
||||||
location=$(pwd)/gradlew
|
location=$(pwd)/gradlew
|
||||||
image=$(pwd)/docs/stirling-transparent.svg
|
image=$(pwd)/docs/stirling-transparent.svg
|
||||||
@@ -235,40 +251,33 @@ EOF
|
|||||||
|
|
||||||
Note: Currently the app will run in the background until manually closed.
|
Note: Currently the app will run in the background until manually closed.
|
||||||
|
|
||||||
### Optional: Changing the Host and Port of the Application
|
### Optional: Changing the host and port of the application:
|
||||||
|
|
||||||
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
|
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
|
||||||
|
|
||||||
```yaml
|
```bash
|
||||||
server:
|
server:
|
||||||
host: 0.0.0.0 # Not working - use instead address
|
host: 0.0.0.0
|
||||||
address: 0.0.0.0
|
|
||||||
port: 3000
|
port: 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
`-Djava.net.preferIPv4Stack=true` --> To force IPv4 only in the Java starting command
|
|
||||||
|
|
||||||
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
|
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
|
||||||
|
|
||||||
### Optional: Run Stirling-PDF as a Service (requires root)
|
### Optional: Run Stirling-PDF as a service (requires root).
|
||||||
|
|
||||||
First create a `.env` file, where you can store environment variables:
|
First create a .env file, where you can store environment variables:
|
||||||
|
```
|
||||||
```bash
|
|
||||||
touch /opt/Stirling-PDF/.env
|
touch /opt/Stirling-PDF/.env
|
||||||
```
|
```
|
||||||
|
In this file you can add all variables, one variable per line, as stated in the main readme (for example SYSTEM_DEFAULTLOCALE="de-DE").
|
||||||
|
|
||||||
In this file, you can add all variables, one variable per line, as stated in the main readme (for example `SYSTEM_DEFAULTLOCALE="de-DE"`).
|
Create a new file where we store our service settings and open it with nano editor:
|
||||||
|
```
|
||||||
Create a new file where we store our service settings and open it with the nano editor:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano /etc/systemd/system/stirlingpdf.service
|
nano /etc/systemd/system/stirlingpdf.service
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste this content, make sure to update the filename of the jar file. Press `Ctrl+S` and `Ctrl+X` to save and exit the nano editor:
|
Paste this content, make sure to update the filename of the jar-file. Press Ctrl+S and Ctrl+X to save and exit the nano editor:
|
||||||
|
```
|
||||||
```ini
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Stirling-PDF service
|
Description=Stirling-PDF service
|
||||||
After=syslog.target network.target
|
After=syslog.target network.target
|
||||||
@@ -292,25 +301,22 @@ WantedBy=multi-user.target
|
|||||||
|
|
||||||
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
|
||||||
|
|
||||||
```bash
|
```
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
```
|
```
|
||||||
|
|
||||||
Enable the service to tell it to start automatically:
|
Enable the service to tell the service to start it automatically:
|
||||||
|
```
|
||||||
```bash
|
|
||||||
sudo systemctl enable stirlingpdf.service
|
sudo systemctl enable stirlingpdf.service
|
||||||
```
|
```
|
||||||
|
|
||||||
See the status of the service:
|
See the status of the service:
|
||||||
|
```
|
||||||
```bash
|
|
||||||
sudo systemctl status stirlingpdf.service
|
sudo systemctl status stirlingpdf.service
|
||||||
```
|
```
|
||||||
|
|
||||||
Manually start/stop/restart the service:
|
Manually start/stop/restart the service:
|
||||||
|
```
|
||||||
```bash
|
|
||||||
sudo systemctl start stirlingpdf.service
|
sudo systemctl start stirlingpdf.service
|
||||||
sudo systemctl stop stirlingpdf.service
|
sudo systemctl stop stirlingpdf.service
|
||||||
sudo systemctl restart stirlingpdf.service
|
sudo systemctl restart stirlingpdf.service
|
||||||
@@ -318,11 +324,12 @@ sudo systemctl restart stirlingpdf.service
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Remember to set the necessary environment variables before running the project if you want to customize the application. The list can be seen in the main readme.
|
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.
|
||||||
|
|
||||||
You can do this in the terminal by using the `export` command or `-D` argument to the Java `-jar` command:
|
You can do this in the terminal by using the `export` command or -D argument to java -jar command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export APP_HOME_NAME="Stirling PDF"
|
export APP_HOME_NAME="Stirling PDF"
|
||||||
or
|
or
|
||||||
-DAPP_HOME_NAME="Stirling PDF"
|
-DAPP_HOME_NAME="Stirling PDF"
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# Pipeline Configuration and Usage Tutorial
|
# Pipeline Configuration and Usage Tutorial
|
||||||
|
- Configure the pipeline config file and input files to run files against it
|
||||||
- Configure the pipeline config file and input files to run files against it.
|
- For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
|
||||||
- For reuse, download the config file and re-upload it when needed, or place it in `/pipeline/defaultWebUIConfigs/` to auto-load in the web UI for all users.
|
|
||||||
|
|
||||||
## Steps to Configure and Use Your Pipeline
|
## Steps to Configure and Use Your Pipeline
|
||||||
|
|
||||||
@@ -27,16 +26,19 @@
|
|||||||
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
|
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
|
||||||
|
|
||||||
8. **Download Pipeline Configuration**
|
8. **Download Pipeline Configuration**
|
||||||
- To use the configuration for folder scanning (or save it for future use and re-upload it), download a JSON file in this menu. You can also pre-load it for future use by placing it in `/pipeline/defaultWebUIConfigs/`. It will then appear in the dropdown menu for all users to use.
|
- To use the configuration for folder scanning (or save it for future use and reupload it), you can also download a JSON file in this menu. You can also pre-load this for future use by placing it in ``/pipeline/defaultWebUIConfigs/``. It will then appear in the dropdown menu for all users to use.
|
||||||
|
|
||||||
9. **Submit Files for Processing**
|
9. **Submit Files for Processing**
|
||||||
- If your pipeline is correctly set up, close the configure menu, input the files, and hit **Submit**.
|
- If your pipeline is correctly set up close the configure menu, input the files and hit **Submit**.
|
||||||
|
|
||||||
10. **Note on Web UI Limitations**
|
10. **Note on Web UI Limitations**
|
||||||
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
|
||||||
|
|
||||||
|
|
||||||
### Current Limitations
|
### Current Limitations
|
||||||
|
- Cannot have more than one of the same operation
|
||||||
|
- Cannot input additional files via UI
|
||||||
|
- All files and operations run in serial mode
|
||||||
|
|
||||||
- Cannot have more than one of the same operation.
|
|
||||||
- Cannot input additional files via UI.
|
|
||||||
- All files and operations run in serial mode.
|
|
||||||
405
README.md
405
README.md
@@ -1,4 +1,4 @@
|
|||||||
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80"></p>
|
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ></p>
|
||||||
<h1 align="center">Stirling-PDF</h1>
|
<h1 align="center">Stirling-PDF</h1>
|
||||||
|
|
||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216®ister=true)
|
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216®ister=true)
|
||||||
|
|
||||||
Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
||||||
|
|
||||||
Stirling-PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes.
|
||||||
|
|
||||||
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Dark mode support
|
- Dark mode support.
|
||||||
- Custom download options
|
- Custom download options
|
||||||
- Parallel file processing and downloads
|
- Parallel file processing and downloads
|
||||||
- Custom 'Pipelines' to run multiple features in a queue
|
- Custom 'Pipelines' to run multiple features in a queue
|
||||||
@@ -27,68 +27,68 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||||
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
||||||
|
|
||||||
## PDF Features
|
## **PDF Features**
|
||||||
|
|
||||||
### Page Operations
|
### **Page Operations**
|
||||||
|
|
||||||
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus on-page edit features like annotate, draw, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
|
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
|
||||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
|
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
|
||||||
- Merge multiple PDFs into a single resultant file
|
- Merge multiple PDFs together into a single resultant file.
|
||||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
|
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
|
||||||
- Reorganize PDF pages into different orders
|
- Reorganize PDF pages into different orders.
|
||||||
- Rotate PDFs in 90-degree increments
|
- Rotate PDFs in 90-degree increments.
|
||||||
- Remove pages
|
- Remove pages.
|
||||||
- Multi-page layout (format PDFs into a multi-paged page)
|
- Multi-page layout (Format PDFs into a multi-paged page).
|
||||||
- Scale page contents size by set percentage
|
- Scale page contents size by set %.
|
||||||
- Adjust contrast
|
- Adjust Contrast.
|
||||||
- Crop PDF
|
- Crop PDF.
|
||||||
- Auto split PDF (with physically scanned page dividers)
|
- Auto Split PDF (With physically scanned page dividers).
|
||||||
- Extract page(s)
|
- Extract page(s).
|
||||||
- Convert PDF to a single page
|
- Convert PDF to a single page.
|
||||||
- Overlay PDFs on top of each other
|
- Overlay PDFs ontop of each other
|
||||||
|
|
||||||
### Conversion Operations
|
### **Conversion Operations**
|
||||||
|
|
||||||
- Convert PDFs to and from images
|
- Convert PDFs to and from images.
|
||||||
- Convert any common file to PDF (using LibreOffice)
|
- Convert any common file to PDF (using LibreOffice).
|
||||||
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
|
- Convert PDF to Word/Powerpoint/Others (using LibreOffice).
|
||||||
- Convert HTML to PDF
|
- Convert HTML to PDF.
|
||||||
- URL to PDF
|
- URL to PDF.
|
||||||
- Markdown to PDF
|
- Markdown to PDF.
|
||||||
|
|
||||||
### Security & Permissions
|
### **Security & Permissions**
|
||||||
|
|
||||||
- Add and remove passwords
|
- Add and remove passwords.
|
||||||
- Change/set PDF permissions
|
- Change/set PDF Permissions.
|
||||||
- Add watermark(s)
|
- Add watermark(s).
|
||||||
- Certify/sign PDFs
|
- Certify/sign PDFs.
|
||||||
- Sanitize PDFs
|
- Sanitize PDFs.
|
||||||
- Auto-redact text
|
- Auto-redact text.
|
||||||
|
|
||||||
### Other Operations
|
### **Other Operations**
|
||||||
|
|
||||||
- Add/generate/write signatures
|
- Add/Generate/Write signatures.
|
||||||
- Repair PDFs
|
- Repair PDFs.
|
||||||
- Detect and remove blank pages
|
- Detect and remove blank pages.
|
||||||
- Compare two PDFs and show differences in text
|
- Compare 2 PDFs and show differences in text.
|
||||||
- Add images to PDFs
|
- Add images to PDFs.
|
||||||
- Compress PDFs to decrease their filesize (using OCRMyPDF)
|
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
|
||||||
- Extract images from PDF
|
- Extract images from PDF.
|
||||||
- Extract images from scans
|
- Extract images from Scans.
|
||||||
- Add page numbers
|
- Add page numbers.
|
||||||
- Auto rename file by detecting PDF header text
|
- Auto rename file by detecting PDF header text.
|
||||||
- OCR on PDF (using OCRMyPDF)
|
- OCR on PDF (Using OCRMyPDF).
|
||||||
- PDF/A conversion (using OCRMyPDF)
|
- PDF/A conversion (Using OCRMyPDF).
|
||||||
- Edit metadata
|
- Edit metadata.
|
||||||
- Flatten PDFs
|
- Flatten PDFs.
|
||||||
- Get all information on a PDF to view or export as JSON
|
- Get all information on a PDF to view or export as JSON.
|
||||||
- Show/detect embedded JavaScript
|
- Show/Detect embedded Javascript
|
||||||
|
|
||||||
For an overview of the tasks and the technology each uses, please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
|
|
||||||
A demo of the app is available [here](https://stirlingpdf.io).
|
Demo of the app is available [here](https://stirlingpdf.io).
|
||||||
|
|
||||||
## Technologies Used
|
## Technologies used
|
||||||
|
|
||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||||
@@ -99,28 +99,27 @@ A demo of the app is available [here](https://stirlingpdf.io).
|
|||||||
- [PDF.js](https://github.com/mozilla/pdf.js)
|
- [PDF.js](https://github.com/mozilla/pdf.js)
|
||||||
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
|
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
|
||||||
|
|
||||||
## How to Use
|
## How to use
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
For windows users download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe)
|
||||||
For Windows users, download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe).
|
|
||||||
|
|
||||||
### Locally
|
### Locally
|
||||||
|
|
||||||
Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md).
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
|
||||||
|
|
||||||
### Docker / Podman
|
### Docker / Podman
|
||||||
|
|
||||||
> [!NOTE]
|
https://hub.docker.com/r/frooodle/s-pdf
|
||||||
> <https://hub.docker.com/r/frooodle/s-pdf>
|
|
||||||
|
|
||||||
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
|
|
||||||
|
|
||||||
|
Stirling PDF has 3 different versions, a Full version and ultra-Lite version as well as a 'Fat' version. Depending on the types of features you use you may want a smaller image to save on space.
|
||||||
|
To see what the different versions offer please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md)
|
||||||
|
For people that don't mind about space optimization just use the latest tag.
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
|
Please note in below examples you may need to change the volume paths as needed, current examples install them to the current working directory
|
||||||
|
eg ``./extraConfigs:/configs`` to ``/opt/stirlingpdf/extraConfigs:/configs``
|
||||||
|
|
||||||
### Docker Run
|
### Docker Run
|
||||||
|
|
||||||
@@ -130,13 +129,15 @@ docker run -d \
|
|||||||
-v ./trainingData:/usr/share/tessdata \
|
-v ./trainingData:/usr/share/tessdata \
|
||||||
-v ./extraConfigs:/configs \
|
-v ./extraConfigs:/configs \
|
||||||
-v ./logs:/logs \
|
-v ./logs:/logs \
|
||||||
# Optional customization (not required)
|
|
||||||
# -v /location/of/customFiles:/customFiles \
|
|
||||||
-e DOCKER_ENABLE_SECURITY=false \
|
-e DOCKER_ENABLE_SECURITY=false \
|
||||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
-e LANGS=en_GB \
|
-e LANGS=en_GB \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
frooodle/s-pdf:latest
|
||||||
|
|
||||||
|
Can also add these for customisation but are not required
|
||||||
|
|
||||||
|
-v /location/of/customFiles:/customFiles \
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
@@ -149,7 +150,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
- ./trainingData:/usr/share/tessdata # Required for extra OCR languages
|
- ./trainingData:/usr/share/tessdata #Required for extra OCR languages
|
||||||
- ./extraConfigs:/configs
|
- ./extraConfigs:/configs
|
||||||
# - ./customFiles:/customFiles/
|
# - ./customFiles:/customFiles/
|
||||||
# - ./logs:/logs/
|
# - ./logs:/logs/
|
||||||
@@ -161,234 +162,196 @@ services:
|
|||||||
|
|
||||||
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
|
||||||
|
|
||||||
## Enable OCR/Compression Feature
|
## Enable OCR/Compression feature
|
||||||
|
|
||||||
Please view the [HowToUseOCR.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md).
|
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md
|
||||||
|
|
||||||
## Reuse Stored Files
|
|
||||||
|
|
||||||
Certain functionality like `Sign` supports pre-saved files stored at `/customFiles/signatures/`. Image files placed within here will be accessible to be used via the web UI. Currently, this supports two folder types:
|
|
||||||
|
|
||||||
- `/customFiles/signatures/ALL_USERS`: Accessible to all users, useful for organizations where many users use the same files or for users not using authentication
|
|
||||||
- `/customFiles/signatures/{username}`: Such as `/customFiles/signatures/froodle`, accessible only to the `froodle` username, private for all others
|
|
||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling-PDF currently supports 36 languages!
|
Stirling PDF currently supports 38!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|
||||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
Please see our [Contributing Guide](CONTRIBUTING.md).
|
Please see our [Contributing Guide](CONTRIBUTING.md)!
|
||||||
|
|
||||||
## Customization
|
## Customisation
|
||||||
|
|
||||||
Stirling-PDF allows easy customization of the app, including things like:
|
Stirling PDF allows easy customization of the app.
|
||||||
|
Includes things like
|
||||||
|
|
||||||
- Custom application name
|
- Custom application name
|
||||||
- Custom slogans, icons, HTML, images, CSS, etc. (via file overrides)
|
- Custom slogans, icons, HTML, images CSS etc (via file overrides)
|
||||||
|
|
||||||
There are two options for this, either using the generated settings file `settings.yml`, which is located in the `/configs` directory and follows standard YAML formatting, or using environment variables, which would override the settings file.
|
There are two options for this, either using the generated settings file ``settings.yml``
|
||||||
|
This file is located in the ``/configs`` directory and follows standard YAML formatting
|
||||||
|
|
||||||
For example, in `settings.yml`, you might have:
|
Environment variables are also supported and would override the settings file
|
||||||
|
For example in the settings.yml you have
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
security:
|
security:
|
||||||
enableLogin: 'true'
|
enableLogin: 'true'
|
||||||
```
|
```
|
||||||
|
|
||||||
To have this via an environment variable, you would use `SECURITY_ENABLELOGIN`.
|
To have this via an environment variable you would have ``SECURITY_ENABLELOGIN``
|
||||||
|
|
||||||
The current list of settings is:
|
The Current list of settings is
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
security:
|
security:
|
||||||
enableLogin: false # set to 'true' to enable login
|
enableLogin: false # set to 'true' to enable login
|
||||||
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
|
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
|
||||||
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
|
||||||
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
|
||||||
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
|
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
|
||||||
initialLogin:
|
initialLogin:
|
||||||
username: '' # initial username for the first login
|
username: '' # Initial username for the first login
|
||||||
password: '' # initial password for the first login
|
password: '' # Initial password for the first login
|
||||||
oauth2:
|
oauth2:
|
||||||
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
|
||||||
client:
|
client:
|
||||||
keycloak:
|
keycloak:
|
||||||
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
|
||||||
clientId: '' # client ID for Keycloak OAuth2
|
clientId: '' # Client ID for Keycloak OAuth2
|
||||||
clientSecret: '' # client secret for Keycloak OAuth2
|
clientSecret: '' # Client Secret for Keycloak OAuth2
|
||||||
scopes: openid, profile, email # scopes for Keycloak OAuth2
|
scopes: openid, profile, email # Scopes for Keycloak OAuth2
|
||||||
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2
|
useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
|
||||||
google:
|
google:
|
||||||
clientId: '' # client ID for Google OAuth2
|
clientId: '' # Client ID for Google OAuth2
|
||||||
clientSecret: '' # client secret for Google OAuth2
|
clientSecret: '' # Client Secret for Google OAuth2
|
||||||
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
|
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2
|
||||||
useAsUsername: email # field to use as the username for Google OAuth2
|
useAsUsername: email # Field to use as the username for Google OAuth2
|
||||||
github:
|
github:
|
||||||
clientId: '' # client ID for GitHub OAuth2
|
clientId: '' # Client ID for GitHub OAuth2
|
||||||
clientSecret: '' # client secret for GitHub OAuth2
|
clientSecret: '' # Client Secret for GitHub OAuth2
|
||||||
scopes: read:user # scope for GitHub OAuth2
|
scopes: read:user # Scope for GitHub OAuth2
|
||||||
useAsUsername: login # field to use as the username for GitHub OAuth2
|
useAsUsername: login # Field to use as the username for GitHub OAuth2
|
||||||
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
|
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
|
||||||
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
|
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'
|
||||||
saml2:
|
|
||||||
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
|
|
||||||
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
|
|
||||||
registrationId: stirling
|
|
||||||
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
|
|
||||||
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
|
|
||||||
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
|
|
||||||
idpIssuer: http://www.okta.com/externalKey
|
|
||||||
idpCert: classpath:okta.crt
|
|
||||||
privateKey: classpath:saml-private-key.key
|
|
||||||
spCert: classpath:saml-public-cert.crt
|
|
||||||
|
|
||||||
enterpriseEdition:
|
|
||||||
enabled: false # set to 'true' to enable enterprise edition
|
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
|
||||||
CustomMetadata:
|
|
||||||
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
|
|
||||||
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
|
|
||||||
creator: Stirling-PDF # supports text such as 'Company-PDF'
|
|
||||||
producer: Stirling-PDF # supports text such as 'Company-PDF'
|
|
||||||
|
|
||||||
legal:
|
|
||||||
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
|
|
||||||
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
|
|
||||||
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
|
|
||||||
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
|
|
||||||
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
|
|
||||||
|
|
||||||
system:
|
system:
|
||||||
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
|
||||||
showUpdate: false # see when a new update is available
|
showUpdate: true # see when a new update is available
|
||||||
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
||||||
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
|
||||||
enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
|
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appName: '' # application's visible name
|
appName: '' # Application's visible name
|
||||||
homeDescription: '' # short description or tagline shown on the homepage
|
homeDescription: '' # Short description or tagline shown on homepage.
|
||||||
appNameNavbar: '' # name displayed on the navigation bar
|
appNameNavbar: '' # Name displayed on the navigation bar
|
||||||
|
|
||||||
endpoints:
|
endpoints:
|
||||||
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
||||||
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
|
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
|
||||||
|
|
||||||
metrics:
|
metrics:
|
||||||
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
|
||||||
|
|
||||||
# Automatically Generated Settings (Do Not Edit Directly)
|
|
||||||
AutomaticallyGenerated:
|
|
||||||
key: example
|
|
||||||
UUID: example
|
|
||||||
```
|
```
|
||||||
|
|
||||||
There is an additional config file `/configs/custom_settings.yml` where users familiar with Java and Spring `application.properties` can input their own settings on top of Stirling-PDF's existing ones.
|
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
|
||||||
|
|
||||||
### Extra Notes
|
### Extra notes
|
||||||
|
|
||||||
- **Endpoints**: Currently, the `ENDPOINTS_TO_REMOVE` and `GROUPS_TO_REMOVE` endpoints can include comma-separated lists of endpoints and groups to disable. For example, `ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages` would disable both image-to-pdf and remove pages, while `GROUPS_TO_REMOVE=LibreOffice` would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
|
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
- **customStaticFilePath**: Customize static files such as the app logo by placing files in the `/customFiles/static/` directory. An example of customizing the app logo is placing `/customFiles/static/favicon.svg` to override the current SVG. This can be used to change any `images/icons/css/fonts/js`, etc. in Stirling-PDF.
|
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
|
||||||
|
|
||||||
### Environment-Only Parameters
|
### Environment only parameters
|
||||||
|
|
||||||
- `SYSTEM_ROOTURIPATH` - Set the application's root URI (e.g. `/pdf-app` to set the root URI to `localhost:8080/pdf-app`)
|
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
|
||||||
- `SYSTEM_CONNECTIONTIMEOUTMINUTES` - Set custom connection timeout values
|
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
|
||||||
- `DOCKER_ENABLE_SECURITY` - Set to `true` to download security jar (required for authentication login)
|
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
|
||||||
- `INSTALL_BOOK_AND_ADVANCED_HTML_OPS` - Download Calibre onto Stirling-PDF to enable PDF to/from book and advanced HTML conversion
|
- ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS`` to download calibre onto stirling-pdf enabling pdf to/from book and advanced html conversion
|
||||||
- `LANGS` - Define custom font libraries to install for document conversions
|
- ``LANGS`` to define custom font libraries to install for use for document conversions
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
For those wanting to use Stirling-PDF's backend API to link with their own custom scripting to edit PDFs, you can view all existing API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/), or navigate to `/swagger-ui/index.html` of your Stirling-PDF instance for your version's documentation (or by following the API button in the settings of Stirling-PDF).
|
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
|
||||||
|
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
|
||||||
|
|
||||||
## Login Authentication
|
## Login authentication
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- User must have the folder `./configs` volumed within Docker so that it is retained during updates.
|
- User must have the folder ./configs volumed within docker so that it is retained during updates.
|
||||||
- Docker users must download the security jar version by setting `DOCKER_ENABLE_SECURITY` to `true` in environment variables.
|
- Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
|
||||||
- Then either enable login via the `settings.yml` file or set `SECURITY_ENABLE_LOGIN` to `true`.
|
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
|
||||||
- Now the initial user will be generated with username `admin` and password `stirling`. On login, you will be forced to change the password to a new one. You can also use the environment variables `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` to set your own credentials straight away (recommended to remove them after user creation).
|
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
|
||||||
|
|
||||||
Once the above has been done, on restart, a new `stirling-pdf-DB.mv.db` will show if everything worked.
|
Once the above has been done, on restart, a new stirling-pdf-DB.mv.db will show if everything worked.
|
||||||
|
|
||||||
When you log in to Stirling-PDF, you will be redirected to the `/login` page to log in with those default credentials. After login, everything should function as normal.
|
When you login to Stirling PDF you will be redirected to /login page to login with those default credentials. After login everything should function as normal
|
||||||
|
|
||||||
To access your account settings, go to Account Settings in the settings cog menu (top right in the navbar). This Account Settings menu is also where you find your API key.
|
To access your account settings go to Account settings in the settings cog menu (top right in navbar) This Account settings menu is also where you find your API key.
|
||||||
|
|
||||||
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
|
To add new users go to the bottom of Account settings and hit 'Admin Settings', here you can add new users. The different roles mentioned within this are for rate limiting. This is a Work in progress which will be expanding on more in future
|
||||||
|
|
||||||
For API usage, you must provide a header with `X-API-Key` and the associated API key for that user.
|
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Q1: What are your planned features?
|
### Q1: What are your planned features?
|
||||||
|
|
||||||
- Progress bar/tracking
|
- Progress bar/Tracking
|
||||||
- Full custom logic pipelines to combine multiple operations together
|
- Full custom logic pipelines to combine multiple operations together.
|
||||||
- Folder support with auto-scanning to perform operations on
|
- Folder support with auto scanning to perform operations on
|
||||||
- Redact text (via UI, not just automated)
|
- Redact text (Via UI not just automated way)
|
||||||
- Add forms
|
- Add Forms
|
||||||
- Multi-page layout (stitch PDF pages together) support x rows y columns and custom page sizing
|
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
|
||||||
- Fill forms manually or automatically
|
- Fill forms manually or automatically
|
||||||
|
|
||||||
### Q2: Why is my application downloading .htm files?
|
### Q2: Why is my application downloading .htm files?
|
||||||
|
|
||||||
This is an issue commonly caused by your NGINX configuration. The default file upload size for NGINX is 1MB. You need to add the following in your Nginx sites-available file: `client_max_body_size SIZE;` (where "SIZE" is 50M for example for 50MB files).
|
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
|
||||||
|
|
||||||
### Q3: Why is my download timing out?
|
### Q3: Why is my download timing out
|
||||||
|
|
||||||
NGINX has timeout values by default, so if you are running Stirling-PDF behind NGINX, you may need to set a timeout value, such as adding the config `proxy_read_timeout 3600;`.
|
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
|
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
|
||||||
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
|
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
|
||||||
|
|
||||||
| Technology | Ultra-Lite | Full |
|
Technology | Ultra-Lite | Full |
|
||||||
| ---------- | :--------: | :---: |
|
| ---------- | :--------: | :---: |
|
||||||
| Java | ✔️ | ✔️ |
|
| Java | ✔️ | ✔️ |
|
||||||
| JavaScript | ✔️ | ✔️ |
|
| JavaScript | ✔️ | ✔️ |
|
||||||
|
|||||||
44
build.gradle
44
build.gradle
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.3.5"
|
id "org.springframework.boot" version "3.3.3"
|
||||||
id "io.spring.dependency-management" version "1.1.6"
|
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"
|
||||||
@@ -13,16 +13,16 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.3.5"
|
springBootVersion = "3.3.3"
|
||||||
pdfboxVersion = "3.0.3"
|
pdfboxVersion = "3.0.3"
|
||||||
logbackVersion = "1.5.7"
|
logbackVersion = "1.5.7"
|
||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.11.0"
|
||||||
lombokVersion = "1.18.34"
|
lombokVersion = "1.18.34"
|
||||||
bouncycastleVersion = "1.78.1"
|
bouncycastleVersion = "1.78.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.31.1"
|
version = "0.29.0"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
@@ -32,10 +32,6 @@ java {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
|
|
||||||
maven {
|
|
||||||
url 'https://build.shibboleth.net/maven/releases'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
@@ -119,7 +115,7 @@ configurations.all {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
//security updates
|
//security updates
|
||||||
implementation "org.springframework:spring-webmvc:6.1.14"
|
implementation "org.springframework:spring-webmvc:6.1.9"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||||
|
|
||||||
@@ -131,37 +127,22 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
||||||
implementation 'com.posthog.java:posthog:1.1.1'
|
|
||||||
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
|
||||||
|
|
||||||
|
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||||
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
runtimeOnly "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
runtimeOnly "com.h2database:h2:2.1.214"
|
runtimeOnly "com.h2database:h2:2.1.214"
|
||||||
// implementation "com.h2database:h2:2.2.224"
|
// implementation "com.h2database:h2:2.2.224"
|
||||||
constraints {
|
|
||||||
implementation "org.opensaml:opensaml-core"
|
|
||||||
implementation "org.opensaml:opensaml-saml-api"
|
|
||||||
implementation "org.opensaml:opensaml-saml-impl"
|
|
||||||
}
|
|
||||||
implementation "org.springframework.security:spring-security-saml2-service-provider"
|
|
||||||
|
|
||||||
implementation 'com.coveo:saml-client:5.0.0'
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
|
||||||
// Batik
|
// Batik
|
||||||
implementation "org.apache.xmlgraphics:batik-all:1.18"
|
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
||||||
|
|
||||||
// TwelveMonkeys
|
// TwelveMonkeys
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
||||||
@@ -181,10 +162,7 @@ dependencies {
|
|||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
||||||
|
|
||||||
// Image metadata extractor
|
implementation "commons-io:commons-io:2.16.1"
|
||||||
implementation "com.drewnoakes:metadata-extractor:2.19.0"
|
|
||||||
|
|
||||||
implementation "commons-io:commons-io:2.17.0"
|
|
||||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||||
//general PDF
|
//general PDF
|
||||||
|
|
||||||
@@ -206,11 +184,11 @@ dependencies {
|
|||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
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.6"
|
implementation "io.micrometer:micrometer-core:1.13.4"
|
||||||
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.24.0"
|
implementation "org.commonmark:commonmark:0.22.0"
|
||||||
implementation "org.commonmark:commonmark-ext-gfm-tables:0.24.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.14.0"
|
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
|
||||||
implementation "com.fathzer:javaluator:3.0.5"
|
implementation "com.fathzer:javaluator:3.0.5"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.31.1
|
appVersion: 0.29.0
|
||||||
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
|
||||||
@@ -13,4 +13,4 @@ maintainers:
|
|||||||
name: stirling-pdf-chart
|
name: stirling-pdf-chart
|
||||||
sources:
|
sources:
|
||||||
- https://github.com/Stirling-Tools/Stirling-PDF
|
- https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
version: 1.0.1
|
version: 1.0.0
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
# stirling-pdf-chart
|
|
||||||
|
|
||||||
 
|
|
||||||
|
|
||||||
locally hosted web application that allows you to perform various operations on PDF files
|
|
||||||
|
|
||||||
**Homepage:** <https://github.com/Stirling-Tools/Stirling-PDF>
|
|
||||||
|
|
||||||
## Maintainers
|
|
||||||
|
|
||||||
| Name | Email | Url |
|
|
||||||
| ---- | ------ | --- |
|
|
||||||
| Stirling-Tools | | <https://github.com/Stirling-Tools/Stirling-PDF> |
|
|
||||||
|
|
||||||
## Source Code
|
|
||||||
|
|
||||||
* <https://github.com/Stirling-Tools/Stirling-PDF>
|
|
||||||
|
|
||||||
## Chart Repo
|
|
||||||
|
|
||||||
Add the following repo to use the chart:
|
|
||||||
|
|
||||||
```console
|
|
||||||
helm repo add stirling-pdf https://stirling-tools.github.io/Stirling-PDF
|
|
||||||
```
|
|
||||||
|
|
||||||
## Values
|
|
||||||
|
|
||||||
| Key | Type | Default | Description |
|
|
||||||
|-----|------|---------|-------------|
|
|
||||||
| affinity | object | `{}` | |
|
|
||||||
| commonLabels | object | `{}` | Labels to apply to all resources |
|
|
||||||
| containerSecurityContext | object | `{}` | |
|
|
||||||
| deployment.annotations | object | `{}` | Stirling-pdf Deployment annotations |
|
|
||||||
| deployment.extraVolumeMounts | list | `[]` | Additional volumes to mount |
|
|
||||||
| deployment.extraVolumes | list | `[]` | Additional volumes |
|
|
||||||
| deployment.labels | object | `{}` | |
|
|
||||||
| deployment.sidecarContainers | object | `{}` | of the chart's content, send notifications... |
|
|
||||||
| envs | list | `[]` | |
|
|
||||||
| extraArgs | list | `[]` | |
|
|
||||||
| image.pullPolicy | string | `"IfNotPresent"` | |
|
|
||||||
| image.repository | string | `"frooodle/s-pdf"` | |
|
|
||||||
| image.tag | string | `nil` | |
|
|
||||||
| ingress | object | `{"annotations":{},"enabled":false,"hosts":[],"ingressClassName":null,"labels":{},"pathType":"ImplementationSpecific"}` | Ingress for load balancer |
|
|
||||||
| ingress.annotations | object | `{}` | Stirling-pdf Ingress annotations |
|
|
||||||
| ingress.hosts | list | `[]` | Must be provided if Ingress is enabled |
|
|
||||||
| ingress.ingressClassName | string | `nil` | See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress |
|
|
||||||
| ingress.labels | object | `{}` | Stirling-pdf Ingress labels |
|
|
||||||
| nodeSelector | object | `{}` | |
|
|
||||||
| persistence.accessMode | string | `"ReadWriteOnce"` | |
|
|
||||||
| persistence.enabled | bool | `false` | |
|
|
||||||
| persistence.labels | object | `{}` | |
|
|
||||||
| persistence.path | string | `"/tmp"` | |
|
|
||||||
| persistence.pv | object | `{"accessMode":"ReadWriteOnce","capacity":{"storage":"8Gi"},"enabled":false,"nfs":{"path":null,"server":null},"pvname":null}` | stirling-pdf data Persistent Volume Storage Class If defined, storageClassName: <storageClass> If set to "-", storageClassName: "", which disables dynamic provisioning If undefined (the default) or set to null, no storageClassName spec is set, choosing the default provisioner. (gp2 on AWS, standard on GKE, AWS & OpenStack) storageClass: "-" volumeName: |
|
|
||||||
| persistence.size | string | `"8Gi"` | |
|
|
||||||
| podAnnotations | object | `{}` | Read more about kube2iam to provide access to s3 https://github.com/jtblin/kube2iam |
|
|
||||||
| podLabels | object | `{}` | ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ |
|
|
||||||
| priorityClassName | string | `""` | |
|
|
||||||
| probes.liveness.failureThreshold | int | `3` | |
|
|
||||||
| probes.liveness.initialDelaySeconds | int | `5` | |
|
|
||||||
| probes.liveness.periodSeconds | int | `10` | |
|
|
||||||
| probes.liveness.successThreshold | int | `1` | |
|
|
||||||
| probes.liveness.timeoutSeconds | int | `1` | |
|
|
||||||
| probes.livenessHttpGetConfig.scheme | string | `"HTTP"` | |
|
|
||||||
| probes.readiness.failureThreshold | int | `3` | |
|
|
||||||
| probes.readiness.initialDelaySeconds | int | `5` | |
|
|
||||||
| probes.readiness.periodSeconds | int | `10` | |
|
|
||||||
| probes.readiness.successThreshold | int | `1` | |
|
|
||||||
| probes.readiness.timeoutSeconds | int | `1` | |
|
|
||||||
| probes.readinessHttpGetConfig.scheme | string | `"HTTP"` | |
|
|
||||||
| replicaCount | int | `1` | |
|
|
||||||
| resources | object | `{}` | |
|
|
||||||
| rootPath | string | `"/"` | Rootpath for the application |
|
|
||||||
| secret.labels | object | `{}` | |
|
|
||||||
| securityContext | object | `{"enabled":true,"fsGroup":1000}` | does not allow this, try setting securityContext: {} |
|
|
||||||
| service.annotations | object | `{}` | |
|
|
||||||
| service.externalPort | int | `8080` | |
|
|
||||||
| service.externalTrafficPolicy | string | `"Local"` | |
|
|
||||||
| service.labels | object | `{}` | |
|
|
||||||
| service.loadBalancerIP | string | `nil` | Only valid if service.type: LoadBalancer |
|
|
||||||
| service.loadBalancerSourceRanges | list | `[]` | Only valid if service.type: LoadBalancer |
|
|
||||||
| service.nodePort | string | `nil` | |
|
|
||||||
| service.servicename | string | `nil` | |
|
|
||||||
| service.targetPort | string | `nil` | from deployment above. Leave empty to use stirling-pdf directly. |
|
|
||||||
| service.type | string | `"ClusterIP"` | |
|
|
||||||
| serviceAccount.annotations | object | `{}` | |
|
|
||||||
| serviceAccount.automountServiceAccountToken | bool | `false` | |
|
|
||||||
| serviceAccount.create | bool | `true` | |
|
|
||||||
| serviceAccount.name | string | `""` | |
|
|
||||||
| serviceMonitor.enabled | bool | `false` | |
|
|
||||||
| serviceMonitor.labels | object | `{}` | |
|
|
||||||
| serviceMonitor.metricsPath | string | `"/metrics"` | |
|
|
||||||
| strategy.type | string | `"RollingUpdate"` | |
|
|
||||||
| tolerations | list | `[]` | |
|
|
||||||
| volumePermissions | object | `{"image":{"pullPolicy":"Always","registry":"docker.io","repository":"bitnami/minideb","tag":"buster"}}` | volumePermissions: Change the owner of the persistent volume mountpoint to RunAsUser:fsGroup |
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{{ template "chart.header" . }}
|
|
||||||
|
|
||||||
{{ template "chart.deprecationWarning" . }}
|
|
||||||
|
|
||||||
{{ template "chart.badgesSection" . }}
|
|
||||||
|
|
||||||
{{ template "chart.description" . }}
|
|
||||||
|
|
||||||
{{ template "chart.homepageLine" . }}
|
|
||||||
|
|
||||||
{{ template "chart.maintainersSection" . }}
|
|
||||||
|
|
||||||
{{ template "chart.sourcesSection" . }}
|
|
||||||
|
|
||||||
{{ template "chart.requirementsSection" . }}
|
|
||||||
|
|
||||||
## Chart Repo
|
|
||||||
|
|
||||||
Add the following repo to use the chart:
|
|
||||||
|
|
||||||
```console
|
|
||||||
helm repo add stirling-pdf https://docs.stirlingpdf.com/Stirling-PDF/
|
|
||||||
```
|
|
||||||
|
|
||||||
{{ template "chart.valuesSection" . }}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
extraArgs:
|
extraArgs: []
|
||||||
[]
|
|
||||||
# - --storage-timestamp-tolerance 1s
|
# - --storage-timestamp-tolerance 1s
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
strategy:
|
strategy:
|
||||||
@@ -11,11 +10,12 @@ image:
|
|||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
secret:
|
secret:
|
||||||
labels: {}
|
labels: {}
|
||||||
# -- Labels to apply to all resources
|
## Labels to apply to all resources
|
||||||
|
##
|
||||||
commonLabels: {}
|
commonLabels: {}
|
||||||
# team_name: dev
|
# team_name: dev
|
||||||
|
|
||||||
# -- Rootpath for the application
|
# rootpath for the application
|
||||||
rootPath: /
|
rootPath: /
|
||||||
|
|
||||||
envs: []
|
envs: []
|
||||||
@@ -31,22 +31,22 @@ envs: []
|
|||||||
# value: "en_GB"
|
# value: "en_GB"
|
||||||
|
|
||||||
deployment:
|
deployment:
|
||||||
# -- Stirling-pdf Deployment annotations
|
## stirling-pdf Deployment annotations
|
||||||
annotations: {}
|
annotations: {}
|
||||||
# name: value
|
# name: value
|
||||||
labels: {}
|
labels: {}
|
||||||
# name: value
|
# name: value
|
||||||
# -- Additional volumes
|
# additional volumes
|
||||||
extraVolumes: []
|
extraVolumes: []
|
||||||
# - name: nginx-config
|
# - name: nginx-config
|
||||||
# secret:
|
# secret:
|
||||||
# secretName: nginx-config
|
# secretName: nginx-config
|
||||||
# -- Additional volumes to mount
|
# additional volumes to mount
|
||||||
extraVolumeMounts: []
|
extraVolumeMounts: []
|
||||||
# -- sidecarContainers for the stirling-pdf
|
## sidecarContainers for the stirling-pdf
|
||||||
# -- Can be used to add a proxy to the pod that does
|
# Can be used to add a proxy to the pod that does
|
||||||
# -- scanning for secrets, signing, authentication, validation
|
# scanning for secrets, signing, authentication, validation
|
||||||
# -- of the chart's content, send notifications...
|
# of the chart's content, send notifications...
|
||||||
sidecarContainers: {}
|
sidecarContainers: {}
|
||||||
## Example sidecarContainer which uses an extraVolume from above and
|
## Example sidecarContainer which uses an extraVolume from above and
|
||||||
## a named port that can be referenced in the service as targetPort.
|
## a named port that can be referenced in the service as targetPort.
|
||||||
@@ -60,34 +60,33 @@ deployment:
|
|||||||
# readOnly: true
|
# readOnly: true
|
||||||
# mountPath: /etc/nginx
|
# mountPath: /etc/nginx
|
||||||
|
|
||||||
# -- Pod annotations
|
## Pod annotations
|
||||||
# -- ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
|
||||||
# -- Read more about kube2iam to provide access to s3 https://github.com/jtblin/kube2iam
|
## Read more about kube2iam to provide access to s3 https://github.com/jtblin/kube2iam
|
||||||
podAnnotations:
|
##
|
||||||
{}
|
podAnnotations: {}
|
||||||
# iam.amazonaws.com/role: role-arn
|
# iam.amazonaws.com/role: role-arn
|
||||||
|
|
||||||
# -- Pod labels
|
## Pod labels
|
||||||
# -- ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
|
||||||
podLabels:
|
podLabels: {}
|
||||||
{}
|
|
||||||
# name: value
|
# name: value
|
||||||
|
|
||||||
service:
|
service:
|
||||||
servicename:
|
servicename:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
externalTrafficPolicy: Local
|
externalTrafficPolicy: Local
|
||||||
# -- Uses pre-assigned IP address from cloud provider
|
## Uses pre-assigned IP address from cloud provider
|
||||||
# -- Only valid if service.type: LoadBalancer
|
## Only valid if service.type: LoadBalancer
|
||||||
loadBalancerIP:
|
loadBalancerIP:
|
||||||
# -- Limits which cidr blocks can connect to service's load balancer
|
## Limits which cidr blocks can connect to service's load balancer
|
||||||
# -- Only valid if service.type: LoadBalancer
|
## Only valid if service.type: LoadBalancer
|
||||||
loadBalancerSourceRanges: []
|
loadBalancerSourceRanges: []
|
||||||
# clusterIP: None
|
# clusterIP: None
|
||||||
externalPort: 8080
|
externalPort: 8080
|
||||||
# -- targetPort of the container to use. If a sidecar should handle the
|
## targetPort of the container to use. If a sidecar should handle the
|
||||||
# -- requests first, use the named port from the sidecar. See sidecar example
|
## requests first, use the named port from the sidecar. See sidecar example
|
||||||
# -- from deployment above. Leave empty to use stirling-pdf directly.
|
## from deployment above. Leave empty to use stirling-pdf directly.
|
||||||
targetPort:
|
targetPort:
|
||||||
nodePort:
|
nodePort:
|
||||||
annotations: {}
|
annotations: {}
|
||||||
@@ -134,10 +133,10 @@ serviceAccount:
|
|||||||
## Annotations for the Service Account
|
## Annotations for the Service Account
|
||||||
annotations: {}
|
annotations: {}
|
||||||
|
|
||||||
# -- UID/GID 1000 is the default user "stirling-pdf" used in
|
# UID/GID 1000 is the default user "stirling-pdf" used in
|
||||||
# -- the container image starting in v0.8.0 and above. This
|
# the container image starting in v0.8.0 and above. This
|
||||||
# -- is required for local persistent storage. If your cluster
|
# is required for local persistent storage. If your cluster
|
||||||
# -- does not allow this, try setting securityContext: {}
|
# does not allow this, try setting securityContext: {}
|
||||||
securityContext:
|
securityContext:
|
||||||
enabled: true
|
enabled: true
|
||||||
fsGroup: 1000
|
fsGroup: 1000
|
||||||
@@ -160,8 +159,7 @@ persistence:
|
|||||||
enabled: false
|
enabled: false
|
||||||
accessMode: ReadWriteOnce
|
accessMode: ReadWriteOnce
|
||||||
size: 8Gi
|
size: 8Gi
|
||||||
labels:
|
labels: {}
|
||||||
{}
|
|
||||||
# name: value
|
# name: value
|
||||||
path: /tmp
|
path: /tmp
|
||||||
## A manually managed Persistent Volume and Claim
|
## A manually managed Persistent Volume and Claim
|
||||||
@@ -169,12 +167,13 @@ persistence:
|
|||||||
## If defined, PVC must be created manually before volume will be bound
|
## If defined, PVC must be created manually before volume will be bound
|
||||||
# existingClaim:
|
# existingClaim:
|
||||||
|
|
||||||
# -- stirling-pdf data Persistent Volume Storage Class
|
## stirling-pdf data Persistent Volume Storage Class
|
||||||
# If defined, storageClassName: <storageClass>
|
## If defined, storageClassName: <storageClass>
|
||||||
# If set to "-", storageClassName: "", which disables dynamic provisioning
|
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||||
# If undefined (the default) or set to null, no storageClassName spec is
|
## If undefined (the default) or set to null, no storageClassName spec is
|
||||||
# set, choosing the default provisioner. (gp2 on AWS, standard on
|
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||||
# GKE, AWS & OpenStack)
|
## GKE, AWS & OpenStack)
|
||||||
|
##
|
||||||
# storageClass: "-"
|
# storageClass: "-"
|
||||||
# volumeName:
|
# volumeName:
|
||||||
pv:
|
pv:
|
||||||
@@ -187,8 +186,9 @@ persistence:
|
|||||||
server:
|
server:
|
||||||
path:
|
path:
|
||||||
|
|
||||||
# -- Init containers parameters:
|
## Init containers parameters:
|
||||||
# -- volumePermissions: Change the owner of the persistent volume mountpoint to RunAsUser:fsGroup
|
## volumePermissions: Change the owner of the persistent volume mountpoint to RunAsUser:fsGroup
|
||||||
|
##
|
||||||
volumePermissions:
|
volumePermissions:
|
||||||
image:
|
image:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
@@ -202,25 +202,25 @@ volumePermissions:
|
|||||||
# pullSecrets:
|
# pullSecrets:
|
||||||
# - myRegistryKeySecretName
|
# - myRegistryKeySecretName
|
||||||
|
|
||||||
# -- Ingress for load balancer
|
## Ingress for load balancer
|
||||||
ingress:
|
ingress:
|
||||||
enabled: false
|
enabled: false
|
||||||
pathType: "ImplementationSpecific"
|
pathType: "ImplementationSpecific"
|
||||||
# -- Stirling-pdf Ingress labels
|
## stirling-pdf Ingress labels
|
||||||
labels:
|
##
|
||||||
{}
|
labels: {}
|
||||||
# dns: "route53"
|
# dns: "route53"
|
||||||
|
|
||||||
# -- Stirling-pdf Ingress annotations
|
## stirling-pdf Ingress annotations
|
||||||
annotations:
|
##
|
||||||
{}
|
annotations: {}
|
||||||
# kubernetes.io/ingress.class: nginx
|
# kubernetes.io/ingress.class: nginx
|
||||||
# kubernetes.io/tls-acme: "true"
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
|
||||||
# -- Stirling-pdf Ingress hostnames
|
## stirling-pdf Ingress hostnames
|
||||||
# -- Must be provided if Ingress is enabled
|
## Must be provided if Ingress is enabled
|
||||||
hosts:
|
##
|
||||||
[]
|
hosts: []
|
||||||
# - name: stirling-pdf.domain1.com
|
# - name: stirling-pdf.domain1.com
|
||||||
# path: /
|
# path: /
|
||||||
# tls: false
|
# tls: false
|
||||||
@@ -234,6 +234,7 @@ ingress:
|
|||||||
# ## Secrets must be added manually to the namespace
|
# ## Secrets must be added manually to the namespace
|
||||||
# tlsSecret: stirling-pdf.domain2-tls
|
# tlsSecret: stirling-pdf.domain2-tls
|
||||||
|
|
||||||
# -- For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
|
# For Kubernetes >= 1.18 you should specify the ingress-controller via the field ingressClassName
|
||||||
# -- See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
|
# See https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/#specifying-the-class-of-an-ingress
|
||||||
ingressClassName:
|
ingressClassName:
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>My First Heading</h1>
|
|
||||||
|
|
||||||
<p>My first paragraph.</p>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
header
|
|
||||||
============
|
|
||||||
|
|
||||||
Header2
|
|
||||||
------------
|
|
||||||
text
|
|
||||||
|
|
||||||
text2
|
|
||||||
|
|
||||||
## **PDF Features**
|
|
||||||
|
|
||||||
### **Page Operations**
|
|
||||||
|
|
||||||
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
|
|
||||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
|
|
||||||
- Merge multiple PDFs together into a single resultant file.
|
|
||||||
Binary file not shown.
@@ -123,7 +123,7 @@ Feature: API Validation
|
|||||||
| odt | .odt |
|
| odt | .odt |
|
||||||
| doc | .doc |
|
| doc | .doc |
|
||||||
|
|
||||||
@ocr @pdfa1
|
@ocr
|
||||||
Scenario: PDFA
|
Scenario: PDFA
|
||||||
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
@@ -134,7 +134,7 @@ Feature: API Validation
|
|||||||
And the response file should have extension ".pdf"
|
And the response file should have extension ".pdf"
|
||||||
And the response file should have size greater than 100
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
@ocr @pdfa2
|
@ocr
|
||||||
Scenario: PDFA1
|
Scenario: PDFA1
|
||||||
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
@@ -218,28 +218,6 @@ Feature: API Validation
|
|||||||
| .odt |
|
| .odt |
|
||||||
| .pptx |
|
| .pptx |
|
||||||
| .rtf |
|
| .rtf |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@calibre @positive @htmltopdf
|
|
||||||
Scenario: Convert HTML to PDF
|
|
||||||
Given I use an example file at "exampleFiles/example.html" as parameter "fileInput"
|
|
||||||
When I send the API request to the endpoint "/api/v1/convert/html/pdf"
|
|
||||||
Then the response status code should be 200
|
|
||||||
And the response file should have size greater than 100
|
|
||||||
And the response file should have extension ".pdf"
|
|
||||||
|
|
||||||
@calibre @positive @zippedhtmltopdf
|
|
||||||
Scenario: Convert zipped HTML to PDF
|
|
||||||
Given I use an example file at "exampleFiles/example_html.zip" as parameter "fileInput"
|
|
||||||
When I send the API request to the endpoint "/api/v1/convert/html/pdf"
|
|
||||||
Then the response status code should be 200
|
|
||||||
And the response file should have size greater than 100
|
|
||||||
And the response file should have extension ".pdf"
|
|
||||||
|
|
||||||
@calibre @positive @markdowntopdf
|
|
||||||
Scenario: Convert Markdown to PDF
|
|
||||||
Given I use an example file at "exampleFiles/example.md" as parameter "fileInput"
|
|
||||||
When I send the API request to the endpoint "/api/v1/convert/markdown/pdf"
|
|
||||||
Then the response status code should be 200
|
|
||||||
And the response file should have size greater than 100
|
|
||||||
And the response file should have extension ".pdf"
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ services:
|
|||||||
limits:
|
limits:
|
||||||
memory: 4G
|
memory: 4G
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 16
|
retries: 16
|
||||||
@@ -19,7 +19,7 @@ services:
|
|||||||
- /stirling/latest/logs:/logs:rw
|
- /stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
PUID: 1002
|
PUID: 1002
|
||||||
PGID: 1002
|
PGID: 1002
|
||||||
UMASK: "022"
|
UMASK: "022"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ ignore = [
|
|||||||
|
|
||||||
[cs_CZ]
|
[cs_CZ]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'pipeline.header',
|
'pipeline.header',
|
||||||
'text',
|
'text',
|
||||||
@@ -78,6 +79,7 @@ ignore = [
|
|||||||
'alphabet',
|
'alphabet',
|
||||||
'compare.document.1',
|
'compare.document.1',
|
||||||
'compare.document.2',
|
'compare.document.2',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'licenses.license',
|
'licenses.license',
|
||||||
'licenses.module',
|
'licenses.module',
|
||||||
@@ -85,6 +87,8 @@ ignore = [
|
|||||||
'licenses.version',
|
'licenses.version',
|
||||||
'pdfOrganiser.mode',
|
'pdfOrganiser.mode',
|
||||||
'pipeline.title',
|
'pipeline.title',
|
||||||
|
'pipelineOptions.pipelineHeader',
|
||||||
|
'sponsor',
|
||||||
'watermark.type.2',
|
'watermark.type.2',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -101,8 +105,11 @@ ignore = [
|
|||||||
[hr_HR]
|
[hr_HR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'PDFToBook.selectText.1',
|
'PDFToBook.selectText.1',
|
||||||
|
'font',
|
||||||
'home.pipeline.title',
|
'home.pipeline.title',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'pdfOrganiser.tags',
|
||||||
'showJS.tags',
|
'showJS.tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -118,6 +125,7 @@ ignore = [
|
|||||||
|
|
||||||
[it_IT]
|
[it_IT]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'font',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'no',
|
'no',
|
||||||
'password',
|
'password',
|
||||||
@@ -140,10 +148,18 @@ ignore = [
|
|||||||
|
|
||||||
[nl_NL]
|
[nl_NL]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'HTMLToPDF.print',
|
||||||
|
'adjustContrast.contrast',
|
||||||
'compare.document.1',
|
'compare.document.1',
|
||||||
'compare.document.2',
|
'compare.document.2',
|
||||||
|
'error',
|
||||||
|
'getPdfInfo.downloadJson',
|
||||||
|
'help',
|
||||||
|
'info',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'navbar.allTools',
|
'navbar.allTools',
|
||||||
|
'printFile.submit',
|
||||||
|
'showJS.downloadJS',
|
||||||
'sponsor',
|
'sponsor',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -165,6 +181,7 @@ ignore = [
|
|||||||
|
|
||||||
[pt_BR]
|
[pt_BR]
|
||||||
ignore = [
|
ignore = [
|
||||||
|
'changeMetadata.trapped',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'pipelineOptions.pipelineHeader',
|
'pipelineOptions.pipelineHeader',
|
||||||
]
|
]
|
||||||
@@ -210,6 +227,7 @@ ignore = [
|
|||||||
[th_TH]
|
[th_TH]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'pipeline.title',
|
||||||
'pipelineOptions.pipelineHeader',
|
'pipelineOptions.pipelineHeader',
|
||||||
'showJS.tags',
|
'showJS.tags',
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Check if a key was provided
|
|
||||||
if [ $# -eq 0 ]; then
|
|
||||||
echo "Please provide a key to remove."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
key_to_remove="$1"
|
|
||||||
|
|
||||||
for file in ../src/main/resources/messages_*.properties; do
|
|
||||||
# If the key ends with a dot, remove all keys starting with it
|
|
||||||
if [[ "$key_to_remove" == *. ]]; then
|
|
||||||
sed -i "/^${key_to_remove//./\\.}/d" "$file"
|
|
||||||
else
|
|
||||||
# Otherwise, remove only the exact key match
|
|
||||||
sed -i "/^${key_to_remove//./\\.}=/d" "$file"
|
|
||||||
fi
|
|
||||||
echo "Updated $file"
|
|
||||||
done
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
translation_key="pdfToPDFA.credit"
|
|
||||||
old_value="OCRmyPDF"
|
|
||||||
new_value="ghostscript"
|
|
||||||
|
|
||||||
for file in ../src/main/resources/messages_*.properties; do
|
|
||||||
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"
|
|
||||||
echo "Updated $file"
|
|
||||||
done
|
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
package stirling.software.SPDF.EE;
|
package stirling.software.SPDF.EE;
|
||||||
|
|
||||||
|
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.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Lazy
|
@Lazy
|
||||||
@Slf4j
|
|
||||||
public class EEAppConfig {
|
public class EEAppConfig {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private static final Logger logger = LoggerFactory.getLogger(EEAppConfig.class);
|
||||||
@Autowired private LicenseKeyChecker licenseKeyChecker;
|
|
||||||
|
|
||||||
@Bean(name = "runningEE")
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Bean(name = "RunningEE")
|
||||||
public boolean runningEnterpriseEdition() {
|
public boolean runningEnterpriseEdition() {
|
||||||
return licenseKeyChecker.getEnterpriseEnabledResult();
|
// TODO: Implement EE detection
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,204 +0,0 @@
|
|||||||
package stirling.software.SPDF.EE;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.http.HttpClient;
|
|
||||||
import java.net.http.HttpRequest;
|
|
||||||
import java.net.http.HttpResponse;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.posthog.java.shaded.org.json.JSONObject;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@Slf4j
|
|
||||||
public class KeygenLicenseVerifier {
|
|
||||||
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
|
||||||
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public KeygenLicenseVerifier(ApplicationProperties applicationProperties) {
|
|
||||||
this.applicationProperties = applicationProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean verifyLicense(String licenseKey) {
|
|
||||||
try {
|
|
||||||
log.info("Checking license key");
|
|
||||||
String machineFingerprint = generateMachineFingerprint();
|
|
||||||
|
|
||||||
// First, try to validate the license
|
|
||||||
JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint);
|
|
||||||
if (validationResponse != null) {
|
|
||||||
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
|
|
||||||
String licenseId = validationResponse.path("data").path("id").asText();
|
|
||||||
if (!isValid) {
|
|
||||||
String code = validationResponse.path("meta").path("code").asText();
|
|
||||||
log.debug(code);
|
|
||||||
if ("NO_MACHINE".equals(code)
|
|
||||||
|| "NO_MACHINES".equals(code)
|
|
||||||
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
|
|
||||||
log.info(
|
|
||||||
"License not activated for this machine. Attempting to activate...");
|
|
||||||
boolean activated =
|
|
||||||
activateMachine(licenseKey, licenseId, machineFingerprint);
|
|
||||||
if (activated) {
|
|
||||||
// Revalidate after activation
|
|
||||||
validationResponse = validateLicense(licenseKey, machineFingerprint);
|
|
||||||
isValid =
|
|
||||||
validationResponse != null
|
|
||||||
&& validationResponse
|
|
||||||
.path("meta")
|
|
||||||
.path("valid")
|
|
||||||
.asBoolean();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isValid;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error verifying license: " + e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonNode validateLicense(String licenseKey, String machineFingerprint)
|
|
||||||
throws Exception {
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
|
||||||
String requestBody =
|
|
||||||
String.format(
|
|
||||||
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
|
||||||
licenseKey, machineFingerprint);
|
|
||||||
HttpRequest request =
|
|
||||||
HttpRequest.newBuilder()
|
|
||||||
.uri(
|
|
||||||
URI.create(
|
|
||||||
BASE_URL
|
|
||||||
+ "/"
|
|
||||||
+ ACCOUNT_ID
|
|
||||||
+ "/licenses/actions/validate-key"))
|
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
|
||||||
.header("Accept", "application/vnd.api+json")
|
|
||||||
// .header("Authorization", "License " + licenseKey)
|
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
log.info(" validateLicenseResponse body: " + response.body());
|
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
|
||||||
if (response.statusCode() == 200) {
|
|
||||||
|
|
||||||
JsonNode metaNode = jsonResponse.path("meta");
|
|
||||||
boolean isValid = metaNode.path("valid").asBoolean();
|
|
||||||
|
|
||||||
String detail = metaNode.path("detail").asText();
|
|
||||||
String code = metaNode.path("code").asText();
|
|
||||||
|
|
||||||
log.debug("License validity: " + isValid);
|
|
||||||
log.debug("Validation detail: " + detail);
|
|
||||||
log.debug("Validation code: " + code);
|
|
||||||
|
|
||||||
int users =
|
|
||||||
jsonResponse
|
|
||||||
.path("data")
|
|
||||||
.path("attributes")
|
|
||||||
.path("metadata")
|
|
||||||
.path("users")
|
|
||||||
.asInt(0);
|
|
||||||
applicationProperties.getEnterpriseEdition().setMaxUsers(users);
|
|
||||||
log.info(applicationProperties.toString());
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.error("Error validating license. Status code: " + response.statusCode());
|
|
||||||
}
|
|
||||||
return jsonResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint)
|
|
||||||
throws Exception {
|
|
||||||
HttpClient client = HttpClient.newHttpClient();
|
|
||||||
|
|
||||||
String hostname;
|
|
||||||
try {
|
|
||||||
hostname = java.net.InetAddress.getLocalHost().getHostName();
|
|
||||||
} catch (Exception e) {
|
|
||||||
hostname = "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
JSONObject body =
|
|
||||||
new JSONObject()
|
|
||||||
.put(
|
|
||||||
"data",
|
|
||||||
new JSONObject()
|
|
||||||
.put("type", "machines")
|
|
||||||
.put(
|
|
||||||
"attributes",
|
|
||||||
new JSONObject()
|
|
||||||
.put("fingerprint", machineFingerprint)
|
|
||||||
.put(
|
|
||||||
"platform",
|
|
||||||
System.getProperty(
|
|
||||||
"os.name")) // Added
|
|
||||||
// platform
|
|
||||||
// parameter
|
|
||||||
.put(
|
|
||||||
"name",
|
|
||||||
hostname)) // Added name parameter
|
|
||||||
.put(
|
|
||||||
"relationships",
|
|
||||||
new JSONObject()
|
|
||||||
.put(
|
|
||||||
"license",
|
|
||||||
new JSONObject()
|
|
||||||
.put(
|
|
||||||
"data",
|
|
||||||
new JSONObject()
|
|
||||||
.put(
|
|
||||||
"type",
|
|
||||||
"licenses")
|
|
||||||
.put(
|
|
||||||
"id",
|
|
||||||
licenseId)))));
|
|
||||||
|
|
||||||
HttpRequest request =
|
|
||||||
HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
|
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
|
||||||
.header("Accept", "application/vnd.api+json")
|
|
||||||
.header(
|
|
||||||
"Authorization",
|
|
||||||
"License " + licenseKey) // Keep the license key authentication
|
|
||||||
.POST(
|
|
||||||
HttpRequest.BodyPublishers.ofString(
|
|
||||||
body.toString())) // Send the JSON body
|
|
||||||
.build();
|
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
log.debug("activateMachine Response body: " + response.body());
|
|
||||||
if (response.statusCode() == 201) {
|
|
||||||
log.info("Machine activated successfully");
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
log.error(
|
|
||||||
"Error activating machine. Status code: {}, error: {}",
|
|
||||||
response.statusCode(),
|
|
||||||
response.body());
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateMachineFingerprint() {
|
|
||||||
return GeneralUtils.generateMachineFingerprint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package stirling.software.SPDF.EE;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class LicenseKeyChecker {
|
|
||||||
|
|
||||||
private final KeygenLicenseVerifier licenseService;
|
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
private boolean enterpriseEnbaledResult = false;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public LicenseKeyChecker(
|
|
||||||
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
|
||||||
this.licenseService = licenseService;
|
|
||||||
this.applicationProperties = applicationProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
|
|
||||||
public void checkLicensePeriodically() {
|
|
||||||
checkLicense();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkLicense() {
|
|
||||||
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
|
||||||
enterpriseEnbaledResult = false;
|
|
||||||
} else {
|
|
||||||
enterpriseEnbaledResult =
|
|
||||||
licenseService.verifyLicense(
|
|
||||||
applicationProperties.getEnterpriseEdition().getKey());
|
|
||||||
if (enterpriseEnbaledResult) {
|
|
||||||
log.info("License key is valid.");
|
|
||||||
} else {
|
|
||||||
log.info("License key is invalid.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateLicenseKey(String newKey) throws IOException {
|
|
||||||
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
|
||||||
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
|
|
||||||
checkLicense();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getEnterpriseEnabledResult() {
|
|
||||||
return enterpriseEnbaledResult;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package stirling.software.SPDF.Factories;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
|
|
||||||
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
|
|
||||||
import stirling.software.SPDF.utils.misc.CustomColorReplaceStrategy;
|
|
||||||
import stirling.software.SPDF.utils.misc.InvertFullColorStrategy;
|
|
||||||
import stirling.software.SPDF.utils.misc.ReplaceAndInvertColorStrategy;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class ReplaceAndInvertColorFactory {
|
|
||||||
|
|
||||||
public ReplaceAndInvertColorStrategy replaceAndInvert(
|
|
||||||
MultipartFile file,
|
|
||||||
ReplaceAndInvert replaceAndInvertOption,
|
|
||||||
HighContrastColorCombination highContrastColorCombination,
|
|
||||||
String backGroundColor,
|
|
||||||
String textColor) {
|
|
||||||
|
|
||||||
if (replaceAndInvertOption == ReplaceAndInvert.CUSTOM_COLOR
|
|
||||||
|| replaceAndInvertOption == ReplaceAndInvert.HIGH_CONTRAST_COLOR) {
|
|
||||||
|
|
||||||
return new CustomColorReplaceStrategy(
|
|
||||||
file,
|
|
||||||
replaceAndInvertOption,
|
|
||||||
textColor,
|
|
||||||
backGroundColor,
|
|
||||||
highContrastColorCombination);
|
|
||||||
|
|
||||||
} else if (replaceAndInvertOption == ReplaceAndInvert.FULL_INVERSION) {
|
|
||||||
|
|
||||||
return new InvertFullColorStrategy(file, replaceAndInvertOption);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,9 +11,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class LibreOfficeListener {
|
public class LibreOfficeListener {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
||||||
@@ -34,7 +31,7 @@ public class LibreOfficeListener {
|
|||||||
private LibreOfficeListener() {}
|
private LibreOfficeListener() {}
|
||||||
|
|
||||||
private boolean isListenerRunning() {
|
private boolean isListenerRunning() {
|
||||||
log.info("waiting for listener to start");
|
System.out.println("waiting for listener to start");
|
||||||
try (Socket socket = new Socket()) {
|
try (Socket socket = new Socket()) {
|
||||||
socket.connect(
|
socket.connect(
|
||||||
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@@ -31,61 +30,30 @@ public class SPdfApplication {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
||||||
|
|
||||||
@Autowired private Environment env;
|
@Autowired private Environment env;
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private static String baseUrlStatic;
|
|
||||||
private static String serverPortStatic;
|
private static String serverPortStatic;
|
||||||
|
|
||||||
@Value("${baseUrl:http://localhost}")
|
|
||||||
private String baseUrl;
|
|
||||||
|
|
||||||
@Value("${server.port:8080}")
|
@Value("${server.port:8080}")
|
||||||
public void setServerPortStatic(String port) {
|
public void setServerPortStatic(String port) {
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
SPdfApplication.serverPortStatic = port;
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
|
||||||
SPdfApplication.serverPortStatic =
|
|
||||||
"0"; // This will let Spring Boot assign an available port
|
|
||||||
} else {
|
|
||||||
SPdfApplication.serverPortStatic = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
|
||||||
private static String findAvailablePort(int startPort) {
|
|
||||||
int port = startPort;
|
|
||||||
while (!isPortAvailable(port)) {
|
|
||||||
port++;
|
|
||||||
}
|
|
||||||
return String.valueOf(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPortAvailable(int port) {
|
|
||||||
try (ServerSocket socket = new ServerSocket(port)) {
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
baseUrlStatic = this.baseUrl;
|
|
||||||
// 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 = baseUrl + ":" + getStaticPort();
|
String url = "http://localhost:" + getNonStaticPort();
|
||||||
|
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
Runtime rt = Runtime.getRuntime();
|
Runtime rt = Runtime.getRuntime();
|
||||||
if (os.contains("win")) {
|
if (os.contains("win")) {
|
||||||
// For Windows
|
// For Windows
|
||||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||||
} else if (os.contains("mac")) {
|
|
||||||
SystemCommand.runCommand(rt, "open " + url);
|
|
||||||
} else if (os.contains("nix") || os.contains("nux")) {
|
|
||||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error opening browser: {}", e.getMessage());
|
logger.error("Error opening browser: {}", e.getMessage());
|
||||||
@@ -101,13 +69,15 @@ public class SPdfApplication {
|
|||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
// External config files
|
// stirling pdf settings file
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||||
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||||
} else {
|
} else {
|
||||||
logger.warn("External configuration file 'configs/settings.yml' does not exist.");
|
logger.warn(
|
||||||
|
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// custom javs settings file
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
@@ -130,31 +100,28 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
// Ensure directories are created
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException("Thread interrupted while sleeping", e);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(Path.of("customFiles/static/"));
|
Files.createDirectories(Path.of("customFiles/static/"));
|
||||||
Files.createDirectories(Path.of("customFiles/templates/"));
|
Files.createDirectories(Path.of("customFiles/templates/"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Error creating directories: {}", e.getMessage());
|
logger.error("Error creating directories: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
printStartupLogs();
|
printStartupLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printStartupLogs() {
|
private static void printStartupLogs() {
|
||||||
logger.info("Stirling-PDF Started.");
|
logger.info("Stirling-PDF Started.");
|
||||||
String url = baseUrlStatic + ":" + getStaticPort();
|
String url = "http://localhost:" + getStaticPort();
|
||||||
logger.info("Navigate to {}", url);
|
logger.info("Navigate to {}", url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getStaticBaseUrl() {
|
|
||||||
return baseUrlStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNonStaticBaseUrl() {
|
|
||||||
return baseUrlStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticPort() {
|
public static String getStaticPort() {
|
||||||
return serverPortStatic;
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.context.annotation.Scope;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
@@ -161,29 +160,4 @@ public class AppConfig {
|
|||||||
public String accessibilityStatement() {
|
public String accessibilityStatement() {
|
||||||
return applicationProperties.getLegal().getAccessibilityStatement();
|
return applicationProperties.getLegal().getAccessibilityStatement();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "analyticsPrompt")
|
|
||||||
@Scope("request")
|
|
||||||
public boolean analyticsPrompt() {
|
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() == null
|
|
||||||
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "analyticsEnabled")
|
|
||||||
@Scope("request")
|
|
||||||
public boolean analyticsEnabled() {
|
|
||||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() != null
|
|
||||||
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "StirlingPDFLabel")
|
|
||||||
public String stirlingPDFLabel() {
|
|
||||||
return "Stirling-PDF" + " v" + appVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "UUID")
|
|
||||||
public String uuid() {
|
|
||||||
return applicationProperties.getAutomaticallyGenerated().getUUID();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
|||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class LocaleConfiguration implements WebMvcConfigurer {
|
public class Beans implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.config.interfaces;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -5,7 +5,6 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -43,7 +42,7 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
public void disableEndpoint(String endpoint) {
|
public void disableEndpoint(String endpoint) {
|
||||||
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
|
||||||
logger.debug("Disabling {}", endpoint);
|
logger.info("Disabling {}", endpoint);
|
||||||
endpointStatuses.put(endpoint, false);
|
endpointStatuses.put(endpoint, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,23 +76,6 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logDisabledEndpointsSummary() {
|
|
||||||
List<String> disabledList =
|
|
||||||
endpointStatuses.entrySet().stream()
|
|
||||||
.filter(entry -> !entry.getValue()) // only get disabled endpoints (value
|
|
||||||
// is false)
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.sorted()
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (!disabledList.isEmpty()) {
|
|
||||||
logger.info(
|
|
||||||
"Total disabled endpoints: {}. Disabled endpoints: {}",
|
|
||||||
disabledList.size(),
|
|
||||||
String.join(", ", disabledList));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void init() {
|
public void init() {
|
||||||
// Adding endpoints to "PageOps" group
|
// Adding endpoints to "PageOps" group
|
||||||
addEndpointToGroup("PageOps", "remove-pages");
|
addEndpointToGroup("PageOps", "remove-pages");
|
||||||
@@ -181,12 +163,14 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
// python
|
// python
|
||||||
addEndpointToGroup("Python", "extract-image-scans");
|
addEndpointToGroup("Python", "extract-image-scans");
|
||||||
|
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");
|
addEndpointToGroup("Python", "pdf-to-img");
|
||||||
|
|
||||||
// openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
|
addEndpointToGroup("OpenCV", REMOVE_BLANKS);
|
||||||
|
|
||||||
// LibreOffice
|
// LibreOffice
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("LibreOffice", "repair");
|
||||||
@@ -246,17 +230,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Javascript", "sign");
|
addEndpointToGroup("Javascript", "sign");
|
||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("Javascript", "compare");
|
||||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||||
|
|
||||||
// Ghostscript dependent endpoints
|
|
||||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
|
||||||
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
|
|
||||||
|
|
||||||
// Weasyprint dependent endpoints
|
|
||||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
|
||||||
addEndpointToGroup("Weasyprint", "url-to-pdf");
|
|
||||||
|
|
||||||
// Pdftohtml dependent endpoints
|
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
@@ -278,9 +251,5 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<String> getEndpointsForGroup(String group) {
|
|
||||||
return endpointGroups.getOrDefault(group, new HashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Slf4j
|
|
||||||
public class ExternalAppDepConfig {
|
|
||||||
@Autowired private EndpointConfiguration endpointConfiguration;
|
|
||||||
|
|
||||||
private boolean isCommandAvailable(String command) {
|
|
||||||
try {
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
processBuilder.command("where", command);
|
|
||||||
} else {
|
|
||||||
processBuilder.command("which", command);
|
|
||||||
}
|
|
||||||
Process process = processBuilder.start();
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
return exitCode == 0;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.debug("Error checking for command {}: {}", command, e.getMessage());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<String, List<String>> commandToGroupMapping =
|
|
||||||
new HashMap<>() {
|
|
||||||
{
|
|
||||||
put("gs", List.of("Ghostscript"));
|
|
||||||
put("soffice", List.of("LibreOffice"));
|
|
||||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
|
||||||
put("weasyprint", List.of("Weasyprint"));
|
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private List<String> getAffectedFeatures(String group) {
|
|
||||||
return endpointConfiguration.getEndpointsForGroup(group).stream()
|
|
||||||
.map(endpoint -> formatEndpointAsFeature(endpoint))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private String formatEndpointAsFeature(String endpoint) {
|
|
||||||
// First replace common terms
|
|
||||||
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
|
|
||||||
|
|
||||||
// Split into words and capitalize each word
|
|
||||||
return Arrays.stream(feature.split("\\s+"))
|
|
||||||
.map(word -> capitalizeWord(word))
|
|
||||||
.collect(Collectors.joining(" "));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String capitalizeWord(String word) {
|
|
||||||
if (word.isEmpty()) {
|
|
||||||
return word;
|
|
||||||
}
|
|
||||||
if ("pdf".equalsIgnoreCase(word)) {
|
|
||||||
return "PDF";
|
|
||||||
}
|
|
||||||
return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkDependencyAndDisableGroup(String command) {
|
|
||||||
boolean isAvailable = isCommandAvailable(command);
|
|
||||||
if (!isAvailable) {
|
|
||||||
List<String> affectedGroups = commandToGroupMapping.get(command);
|
|
||||||
|
|
||||||
if (affectedGroups != null) {
|
|
||||||
for (String group : affectedGroups) {
|
|
||||||
List<String> affectedFeatures = getAffectedFeatures(group);
|
|
||||||
endpointConfiguration.disableGroup(group);
|
|
||||||
log.warn(
|
|
||||||
"Missing dependency: {} - Disabling group: {} (Affected features: {})",
|
|
||||||
command,
|
|
||||||
group,
|
|
||||||
affectedFeatures != null && !affectedFeatures.isEmpty()
|
|
||||||
? String.join(", ", affectedFeatures)
|
|
||||||
: "unknown");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void checkDependencies() {
|
|
||||||
|
|
||||||
// Check core dependencies
|
|
||||||
checkDependencyAndDisableGroup("gs");
|
|
||||||
checkDependencyAndDisableGroup("soffice");
|
|
||||||
checkDependencyAndDisableGroup("ocrmypdf");
|
|
||||||
checkDependencyAndDisableGroup("weasyprint");
|
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
|
||||||
|
|
||||||
// Special handling for Python/OpenCV dependencies
|
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
|
||||||
if (!pythonAvailable) {
|
|
||||||
List<String> pythonFeatures = getAffectedFeatures("Python");
|
|
||||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
|
||||||
|
|
||||||
endpointConfiguration.disableGroup("Python");
|
|
||||||
endpointConfiguration.disableGroup("OpenCV");
|
|
||||||
log.warn(
|
|
||||||
"Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}",
|
|
||||||
String.join(", ", pythonFeatures),
|
|
||||||
String.join(", ", openCVFeatures));
|
|
||||||
} else {
|
|
||||||
// If Python is available, check for OpenCV
|
|
||||||
try {
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder();
|
|
||||||
if (System.getProperty("os.name").toLowerCase().contains("windows")) {
|
|
||||||
processBuilder.command("python", "-c", "import cv2");
|
|
||||||
} else {
|
|
||||||
processBuilder.command("python3", "-c", "import cv2");
|
|
||||||
}
|
|
||||||
Process process = processBuilder.start();
|
|
||||||
int exitCode = process.waitFor();
|
|
||||||
if (exitCode != 0) {
|
|
||||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
|
||||||
endpointConfiguration.disableGroup("OpenCV");
|
|
||||||
log.warn(
|
|
||||||
"OpenCV not available in Python - Disabling OpenCV features: {}",
|
|
||||||
String.join(", ", openCVFeatures));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
List<String> openCVFeatures = getAffectedFeatures("OpenCV");
|
|
||||||
endpointConfiguration.disableGroup("OpenCV");
|
|
||||||
log.warn(
|
|
||||||
"Error checking OpenCV: {} - Disabling OpenCV features: {}",
|
|
||||||
e.getMessage(),
|
|
||||||
String.join(", ", openCVFeatures));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
endpointConfiguration.logDisabledEndpointsSummary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import io.micrometer.common.util.StringUtils;
|
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
|
||||||
public class InitialSetup {
|
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initUUIDKey() throws IOException {
|
|
||||||
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
|
||||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
|
||||||
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
|
||||||
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initSecretKey() throws IOException {
|
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
|
||||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
|
||||||
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initLegalUrls() throws IOException {
|
|
||||||
// Initialize Terms and Conditions
|
|
||||||
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
|
||||||
if (StringUtils.isEmpty(termsUrl)) {
|
|
||||||
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
|
||||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
|
|
||||||
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Privacy Policy
|
|
||||||
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
|
||||||
if (StringUtils.isEmpty(privacyUrl)) {
|
|
||||||
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
|
||||||
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
|
|
||||||
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ 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 jakarta.servlet.http.HttpSession;
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -33,11 +32,10 @@ public class MetricsFilter extends OncePerRequestFilter {
|
|||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|
||||||
if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) {
|
if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) {
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
String sessionId = (session != null) ? session.getId() : "no-session";
|
|
||||||
Counter counter =
|
Counter counter =
|
||||||
Counter.builder("http.requests")
|
Counter.builder("http.requests")
|
||||||
.tag("session", sessionId)
|
.tag("session", request.getSession().getId())
|
||||||
.tag("method", request.getMethod())
|
.tag("method", request.getMethod())
|
||||||
.tag("uri", uri)
|
.tag("uri", uri)
|
||||||
.register(meterRegistry);
|
.register(meterRegistry);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.service;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
@@ -15,16 +15,16 @@ import stirling.software.SPDF.model.PdfMetadata;
|
|||||||
public class PdfMetadataService {
|
public class PdfMetadataService {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final String stirlingPDFLabel;
|
private final String appVersion;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PdfMetadataService(
|
public PdfMetadataService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
@Qualifier("appVersion") String appVersion,
|
||||||
@Autowired(required = false) UserServiceInterface userService) {
|
@Autowired(required = false) UserServiceInterface userService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.stirlingPDFLabel = stirlingPDFLabel;
|
this.appVersion = appVersion;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,40 +59,51 @@ public class PdfMetadataService {
|
|||||||
|
|
||||||
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
|
|
||||||
String creator = stirlingPDFLabel;
|
String creator = "Stirling-PDF";
|
||||||
|
|
||||||
if (applicationProperties
|
// if (applicationProperties
|
||||||
.getEnterpriseEdition()
|
// .getEnterpriseEdition()
|
||||||
.getCustomMetadata()
|
// .getCustomMetadata()
|
||||||
.isAutoUpdateMetadata()) {
|
// .isAutoUpdateMetadata()) {
|
||||||
|
|
||||||
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
// producer =
|
||||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
//
|
||||||
}
|
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer();
|
||||||
|
// creator =
|
||||||
|
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
||||||
|
// title = applicationProperties.getEnterpriseEdition().getCustomMetadata().getTitle();
|
||||||
|
|
||||||
pdf.getDocumentInformation().setCreator(creator);
|
// if ("{filename}".equals(title)) {
|
||||||
|
// title = "Filename"; // Replace with actual filename logic
|
||||||
|
// } else if ("{unchanged}".equals(title)) {
|
||||||
|
// title = pdfMetadata.getTitle(); // Keep the original title
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
pdf.getDocumentInformation().setCreator(creator + " " + appVersion);
|
||||||
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
|
String producer = "Stirling-PDF";
|
||||||
String title = pdfMetadata.getTitle();
|
String title = pdfMetadata.getTitle();
|
||||||
pdf.getDocumentInformation().setTitle(title);
|
pdf.getDocumentInformation().setTitle(title);
|
||||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
pdf.getDocumentInformation().setProducer(producer + " " + appVersion);
|
||||||
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
||||||
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
||||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||||
|
|
||||||
String author = pdfMetadata.getAuthor();
|
String author = pdfMetadata.getAuthor();
|
||||||
if (applicationProperties
|
// if (applicationProperties
|
||||||
.getEnterpriseEdition()
|
// .getEnterpriseEdition()
|
||||||
.getCustomMetadata()
|
// .getCustomMetadata()
|
||||||
.isAutoUpdateMetadata()) {
|
// .isAutoUpdateMetadata()) {
|
||||||
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
// author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
||||||
|
|
||||||
if (userService != null) {
|
// if (userService != null) {
|
||||||
author = author.replace("username", userService.getCurrentUsername());
|
// author = author.replace("username", userService.getCurrentUsername());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
pdf.getDocumentInformation().setAuthor(author);
|
pdf.getDocumentInformation().setAuthor(author);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
package stirling.software.SPDF.config;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import com.posthog.java.PostHog;
|
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class PostHogConfig {
|
|
||||||
|
|
||||||
@Value("${posthog.api.key}")
|
|
||||||
private String posthogApiKey;
|
|
||||||
|
|
||||||
@Value("${posthog.host}")
|
|
||||||
private String posthogHost;
|
|
||||||
|
|
||||||
private PostHog postHogClient;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public PostHog postHogClient() {
|
|
||||||
postHogClient = new PostHog.Builder(posthogApiKey).host(posthogHost).build();
|
|
||||||
return postHogClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreDestroy
|
|
||||||
public void shutdownPostHog() {
|
|
||||||
if (postHogClient != null) {
|
|
||||||
postHogClient.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.config.interfaces;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
public interface ShowAdminInterface {
|
public interface ShowAdminInterface {
|
||||||
default boolean getShowUpdateOnlyAdmins() {
|
default boolean getShowUpdateOnlyAdmins() {
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
// package stirling.software.SPDF.config.fingerprint;
|
|
||||||
//
|
|
||||||
// import java.io.IOException;
|
|
||||||
//
|
|
||||||
// import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
// import org.springframework.stereotype.Component;
|
|
||||||
// import org.springframework.web.filter.OncePerRequestFilter;
|
|
||||||
//
|
|
||||||
// import jakarta.servlet.FilterChain;
|
|
||||||
// import jakarta.servlet.ServletException;
|
|
||||||
// import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
// import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
// import jakarta.servlet.http.HttpSession;
|
|
||||||
// import lombok.extern.slf4j.Slf4j;
|
|
||||||
// import stirling.software.SPDF.utils.RequestUriUtils;
|
|
||||||
//
|
|
||||||
//// @Component
|
|
||||||
// @Slf4j
|
|
||||||
// public class FingerprintBasedSessionFilter extends OncePerRequestFilter {
|
|
||||||
// private final FingerprintGenerator fingerprintGenerator;
|
|
||||||
// private final FingerprintBasedSessionManager sessionManager;
|
|
||||||
//
|
|
||||||
// @Autowired
|
|
||||||
// public FingerprintBasedSessionFilter(
|
|
||||||
// FingerprintGenerator fingerprintGenerator,
|
|
||||||
// FingerprintBasedSessionManager sessionManager) {
|
|
||||||
// this.fingerprintGenerator = fingerprintGenerator;
|
|
||||||
// this.sessionManager = sessionManager;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// protected void doFilterInternal(
|
|
||||||
// HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
|
||||||
// throws ServletException, IOException {
|
|
||||||
//
|
|
||||||
// if (RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
|
||||||
// filterChain.doFilter(request, response);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// String fingerprint = fingerprintGenerator.generateFingerprint(request);
|
|
||||||
// log.debug("Generated fingerprint for request: {}", fingerprint);
|
|
||||||
//
|
|
||||||
// HttpSession session = request.getSession();
|
|
||||||
// boolean isNewSession = session.isNew();
|
|
||||||
// String sessionId = session.getId();
|
|
||||||
//
|
|
||||||
// if (isNewSession) {
|
|
||||||
// log.info("New session created: {}", sessionId);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (!sessionManager.isFingerPrintAllowed(fingerprint)) {
|
|
||||||
// log.info("Blocked fingerprint detected, redirecting: {}", fingerprint);
|
|
||||||
// response.sendRedirect(request.getContextPath() + "/too-many-requests");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// session.setAttribute("userFingerprint", fingerprint);
|
|
||||||
// session.setAttribute(
|
|
||||||
// FingerprintBasedSessionManager.STARTUP_TIMESTAMP,
|
|
||||||
// FingerprintBasedSessionManager.APP_STARTUP_TIME);
|
|
||||||
//
|
|
||||||
// sessionManager.registerFingerprint(fingerprint, sessionId);
|
|
||||||
//
|
|
||||||
// log.debug("Proceeding with request: {}", request.getRequestURI());
|
|
||||||
// filterChain.doFilter(request, response);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
// package stirling.software.SPDF.config.fingerprint;
|
|
||||||
//
|
|
||||||
// import java.util.Iterator;
|
|
||||||
// import java.util.Map;
|
|
||||||
// import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
// import java.util.concurrent.TimeUnit;
|
|
||||||
//
|
|
||||||
// import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
// import org.springframework.stereotype.Component;
|
|
||||||
//
|
|
||||||
// import jakarta.servlet.http.HttpSession;
|
|
||||||
// import jakarta.servlet.http.HttpSessionAttributeListener;
|
|
||||||
// import jakarta.servlet.http.HttpSessionEvent;
|
|
||||||
// import jakarta.servlet.http.HttpSessionListener;
|
|
||||||
// import lombok.AllArgsConstructor;
|
|
||||||
// import lombok.Data;
|
|
||||||
// import lombok.extern.slf4j.Slf4j;
|
|
||||||
//
|
|
||||||
// @Slf4j
|
|
||||||
// @Component
|
|
||||||
// public class FingerprintBasedSessionManager
|
|
||||||
// implements HttpSessionListener, HttpSessionAttributeListener {
|
|
||||||
// private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints =
|
|
||||||
// new ConcurrentHashMap<>();
|
|
||||||
//
|
|
||||||
// // To be reduced in later version to 8~
|
|
||||||
// private static final int MAX_ACTIVE_FINGERPRINTS = 30;
|
|
||||||
//
|
|
||||||
// static final String STARTUP_TIMESTAMP = "appStartupTimestamp";
|
|
||||||
// static final long APP_STARTUP_TIME = System.currentTimeMillis();
|
|
||||||
// private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30);
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void sessionCreated(HttpSessionEvent se) {
|
|
||||||
// HttpSession session = se.getSession();
|
|
||||||
// String sessionId = session.getId();
|
|
||||||
// String fingerprint = (String) session.getAttribute("userFingerprint");
|
|
||||||
//
|
|
||||||
// if (fingerprint == null) {
|
|
||||||
// log.warn("Session created without fingerprint: {}", sessionId);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS
|
|
||||||
// && !activeFingerprints.containsKey(fingerprint)) {
|
|
||||||
// log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId);
|
|
||||||
// session.setAttribute("blocked", true);
|
|
||||||
// } else {
|
|
||||||
// activeFingerprints.put(
|
|
||||||
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
|
|
||||||
// log.info(
|
|
||||||
// "New fingerprint registered: {}. Total active fingerprints: {}",
|
|
||||||
// fingerprint,
|
|
||||||
// activeFingerprints.size());
|
|
||||||
// }
|
|
||||||
// session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void sessionDestroyed(HttpSessionEvent se) {
|
|
||||||
// HttpSession session = se.getSession();
|
|
||||||
// String fingerprint = (String) session.getAttribute("userFingerprint");
|
|
||||||
//
|
|
||||||
// if (fingerprint != null) {
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// activeFingerprints.remove(fingerprint);
|
|
||||||
// log.info(
|
|
||||||
// "Fingerprint removed: {}. Total active fingerprints: {}",
|
|
||||||
// fingerprint,
|
|
||||||
// activeFingerprints.size());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public boolean isFingerPrintAllowed(String fingerprint) {
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS
|
|
||||||
// || activeFingerprints.containsKey(fingerprint);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public void registerFingerprint(String fingerprint, String sessionId) {
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// activeFingerprints.put(
|
|
||||||
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public void unregisterFingerprint(String fingerprint) {
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// activeFingerprints.remove(fingerprint);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Scheduled(fixedRate = 1800000) // Run every 30 mins
|
|
||||||
// public void cleanupStaleFingerprints() {
|
|
||||||
// log.info("Starting cleanup of stale fingerprints");
|
|
||||||
// long now = System.currentTimeMillis();
|
|
||||||
// int removedCount = 0;
|
|
||||||
//
|
|
||||||
// synchronized (activeFingerprints) {
|
|
||||||
// Iterator<Map.Entry<String, FingerprintInfo>> iterator =
|
|
||||||
// activeFingerprints.entrySet().iterator();
|
|
||||||
// while (iterator.hasNext()) {
|
|
||||||
// Map.Entry<String, FingerprintInfo> entry = iterator.next();
|
|
||||||
// FingerprintInfo info = entry.getValue();
|
|
||||||
//
|
|
||||||
// if (now - info.getLastAccessTime() > FINGERPRINT_EXPIRATION) {
|
|
||||||
// iterator.remove();
|
|
||||||
// removedCount++;
|
|
||||||
// log.info("Removed stale fingerprint: {}", entry.getKey());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// log.info("Cleanup complete. Removed {} stale fingerprints", removedCount);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// public void updateLastAccessTime(String fingerprint) {
|
|
||||||
// FingerprintInfo info = activeFingerprints.get(fingerprint);
|
|
||||||
// if (info != null) {
|
|
||||||
// info.setLastAccessTime(System.currentTimeMillis());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Data
|
|
||||||
// @AllArgsConstructor
|
|
||||||
// private static class FingerprintInfo {
|
|
||||||
// private String sessionId;
|
|
||||||
// private long lastAccessTime;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// package stirling.software.SPDF.config.fingerprint;
|
|
||||||
//
|
|
||||||
// import java.security.MessageDigest;
|
|
||||||
// import java.security.NoSuchAlgorithmException;
|
|
||||||
//
|
|
||||||
// import org.springframework.stereotype.Component;
|
|
||||||
//
|
|
||||||
// import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
//
|
|
||||||
// @Component
|
|
||||||
// public class FingerprintGenerator {
|
|
||||||
//
|
|
||||||
// public String generateFingerprint(HttpServletRequest request) {
|
|
||||||
// if (request == null) {
|
|
||||||
// return "";
|
|
||||||
// }
|
|
||||||
// StringBuilder fingerprintBuilder = new StringBuilder();
|
|
||||||
//
|
|
||||||
// // Add IP address
|
|
||||||
// fingerprintBuilder.append(request.getRemoteAddr());
|
|
||||||
//
|
|
||||||
// // Add X-Forwarded-For header if present (for clients behind proxies)
|
|
||||||
// String forwardedFor = request.getHeader("X-Forwarded-For");
|
|
||||||
// if (forwardedFor != null) {
|
|
||||||
// fingerprintBuilder.append(forwardedFor);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add User-Agent
|
|
||||||
// String userAgent = request.getHeader("User-Agent");
|
|
||||||
// if (userAgent != null) {
|
|
||||||
// fingerprintBuilder.append(userAgent);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add Accept-Language header
|
|
||||||
// String acceptLanguage = request.getHeader("Accept-Language");
|
|
||||||
// if (acceptLanguage != null) {
|
|
||||||
// fingerprintBuilder.append(acceptLanguage);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add Accept header
|
|
||||||
// String accept = request.getHeader("Accept");
|
|
||||||
// if (accept != null) {
|
|
||||||
// fingerprintBuilder.append(accept);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add Connection header
|
|
||||||
// String connection = request.getHeader("Connection");
|
|
||||||
// if (connection != null) {
|
|
||||||
// fingerprintBuilder.append(connection);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Add server port
|
|
||||||
// fingerprintBuilder.append(request.getServerPort());
|
|
||||||
//
|
|
||||||
// // Add secure flag
|
|
||||||
// fingerprintBuilder.append(request.isSecure());
|
|
||||||
//
|
|
||||||
// // Generate a hash of the fingerprint
|
|
||||||
// return generateHash(fingerprintBuilder.toString());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private String generateHash(String input) {
|
|
||||||
// try {
|
|
||||||
// MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
||||||
// byte[] hash = digest.digest(input.getBytes());
|
|
||||||
// StringBuilder hexString = new StringBuilder();
|
|
||||||
// for (byte b : hash) {
|
|
||||||
// String hex = Integer.toHexString(0xff & b);
|
|
||||||
// if (hex.length() == 1) hexString.append('0');
|
|
||||||
// hexString.append(hex);
|
|
||||||
// }
|
|
||||||
// return hexString.toString();
|
|
||||||
// } catch (NoSuchAlgorithmException e) {
|
|
||||||
// throw new RuntimeException("Failed to generate fingerprint hash", e);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
@@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
import stirling.software.SPDF.config.ShowAdminInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|||||||
@@ -1,237 +1,27 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import com.coveo.saml.SamlClient;
|
|
||||||
|
|
||||||
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.AllArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.SPdfApplication;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
|
||||||
import stirling.software.SPDF.model.Provider;
|
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@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 {
|
||||||
|
|
||||||
if (!response.isCommitted()) {
|
if (request.getParameter("userIsDisabled") != null) {
|
||||||
// Handle user logout due to disabled account
|
getRedirectStrategy()
|
||||||
if (request.getParameter("userIsDisabled") != null) {
|
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
|
||||||
response.sendRedirect(
|
return;
|
||||||
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle OAuth2 authentication error
|
|
||||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
|
||||||
response.sendRedirect(
|
|
||||||
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authentication != null) {
|
|
||||||
// Handle SAML2 logout redirection
|
|
||||||
if (authentication instanceof Saml2Authentication) {
|
|
||||||
getRedirect_saml2(request, response, authentication);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle OAuth2 logout redirection
|
|
||||||
else if (authentication instanceof OAuth2AuthenticationToken) {
|
|
||||||
getRedirect_oauth2(request, response, authentication);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle Username/Password logout
|
|
||||||
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle unknown authentication types
|
|
||||||
else {
|
|
||||||
log.error(
|
|
||||||
"authentication class unknown: "
|
|
||||||
+ authentication.getClass().getSimpleName());
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Redirect to login page after logout
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect for SAML2 authentication logout
|
|
||||||
private void getRedirect_saml2(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
|
||||||
String registrationId = samlConf.getRegistrationId();
|
|
||||||
|
|
||||||
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
|
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
|
||||||
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
|
||||||
|
|
||||||
String nameIdValue = principal.getName();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read certificate from the resource
|
|
||||||
Resource certificateResource = samlConf.getSpCert();
|
|
||||||
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);
|
|
||||||
|
|
||||||
List<X509Certificate> certificates = new ArrayList<>();
|
|
||||||
certificates.add(certificate);
|
|
||||||
|
|
||||||
// Construct URLs required for SAML configuration
|
|
||||||
String serverUrl =
|
|
||||||
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
|
|
||||||
|
|
||||||
String relyingPartyIdentifier =
|
|
||||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
|
||||||
|
|
||||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
|
||||||
|
|
||||||
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
|
||||||
|
|
||||||
String idpIssuer = samlConf.getIdpIssuer();
|
|
||||||
|
|
||||||
// Create SamlClient instance for SAML logout
|
|
||||||
SamlClient samlClient =
|
|
||||||
new SamlClient(
|
|
||||||
relyingPartyIdentifier,
|
|
||||||
assertionConsumerServiceUrl,
|
|
||||||
idpUrl,
|
|
||||||
idpIssuer,
|
|
||||||
certificates,
|
|
||||||
SamlClient.SamlIdpBinding.POST);
|
|
||||||
|
|
||||||
// Read private key for service provider
|
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
|
||||||
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);
|
|
||||||
|
|
||||||
// Set service provider keys for the SamlClient
|
|
||||||
samlClient.setSPKeys(certificate, privateKey);
|
|
||||||
|
|
||||||
// Redirect to identity provider for logout
|
|
||||||
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(nameIdValue, e);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect for OAuth2 authentication logout
|
|
||||||
private void getRedirect_oauth2(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws IOException {
|
|
||||||
String param = "logout=true";
|
|
||||||
String registrationId = null;
|
|
||||||
String issuer = null;
|
|
||||||
String clientId = null;
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
|
||||||
|
|
||||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
|
||||||
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
|
||||||
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get OAuth2 provider details from configuration
|
|
||||||
Provider provider = oauth.getClient().get(registrationId);
|
|
||||||
issuer = provider.getIssuer();
|
|
||||||
clientId = provider.getClientId();
|
|
||||||
} catch (UnsupportedProviderException e) {
|
|
||||||
log.error(e.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
|
||||||
issuer = oauth.getIssuer();
|
|
||||||
clientId = oauth.getClientId();
|
|
||||||
}
|
|
||||||
String errorMessage = "";
|
|
||||||
// Handle different error scenarios during logout
|
|
||||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
|
||||||
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
|
||||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
|
||||||
param = "error=" + sanitizeInput(errorMessage);
|
|
||||||
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
|
||||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
|
||||||
} 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;
|
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||||
|
|
||||||
// Redirect based on OAuth2 provider
|
|
||||||
switch (registrationId.toLowerCase()) {
|
|
||||||
case "keycloak":
|
|
||||||
// Add Keycloak specific logout URL if needed
|
|
||||||
String logoutUrl =
|
|
||||||
issuer
|
|
||||||
+ "/protocol/openid-connect/logout"
|
|
||||||
+ "?client_id="
|
|
||||||
+ clientId
|
|
||||||
+ "&post_logout_redirect_uri="
|
|
||||||
+ response.encodeRedirectURL(redirect_url);
|
|
||||||
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
|
||||||
response.sendRedirect(logoutUrl);
|
|
||||||
break;
|
|
||||||
case "github":
|
|
||||||
// Add GitHub specific logout URL if needed
|
|
||||||
String githubLogoutUrl = "https://github.com/logout";
|
|
||||||
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
|
||||||
response.sendRedirect(githubLogoutUrl);
|
|
||||||
break;
|
|
||||||
case "google":
|
|
||||||
// Add Google specific logout URL if needed
|
|
||||||
// String googleLogoutUrl =
|
|
||||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
|
||||||
// + response.encodeRedirectURL(redirect_url);
|
|
||||||
log.info("Google does not have a specific logout URL");
|
|
||||||
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
|
||||||
// break;
|
|
||||||
default:
|
|
||||||
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
|
||||||
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
|
||||||
response.sendRedirect(defaultRedirectUrl);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize input to avoid potential security vulnerabilities
|
|
||||||
private String sanitizeInput(String input) {
|
|
||||||
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -16,12 +14,9 @@ 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 jakarta.servlet.http.HttpSession;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Component
|
@Component
|
||||||
public class FirstLoginFilter extends OncePerRequestFilter {
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@@ -55,22 +50,6 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
|
||||||
HttpSession session = request.getSession(true);
|
|
||||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
|
|
||||||
String creationTime = timeFormat.format(new Date(session.getCreationTime()));
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
|
|
||||||
session.isNew(),
|
|
||||||
creationTime,
|
|
||||||
session.getId(),
|
|
||||||
request.getRemoteAddr(),
|
|
||||||
request.getHeader("User-Agent"),
|
|
||||||
request.getHeader("Referer"),
|
|
||||||
request.getRequestURL().toString());
|
|
||||||
}
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@@ -34,6 +39,15 @@ public class InitialSecuritySetup {
|
|||||||
initializeInternalApiUser();
|
initializeInternalApiUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initSecretKey() throws IOException {
|
||||||
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
|
if (!isValidUUID(secretKey)) {
|
||||||
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
|
saveKeyToConfig(secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void initializeAdminUser() throws IOException {
|
private void initializeAdminUser() throws IOException {
|
||||||
String initialUsername =
|
String initialUsername =
|
||||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
@@ -75,4 +89,33 @@ public class InitialSecuritySetup {
|
|||||||
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveKeyToConfig(String key) throws IOException {
|
||||||
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
|
||||||
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
||||||
|
yamlOptionssettingsYml.setSplitLines(false);
|
||||||
|
|
||||||
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
settingsYml
|
||||||
|
.path("AutomaticallyGenerated.key")
|
||||||
|
.set(key)
|
||||||
|
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
|
settingsYml.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidUUID(String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
@@ -26,34 +24,20 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio
|
|||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
|
||||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
|
||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
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.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
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.ApplicationProperties.Security.SAML2;
|
|
||||||
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;
|
||||||
@@ -63,11 +47,12 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
@Slf4j
|
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
@@ -90,48 +75,11 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
http.addFilterBefore(
|
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
|
||||||
http.csrf(csrf -> csrf.disable());
|
|
||||||
} else {
|
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
|
||||||
new CsrfTokenRequestAttributeHandler();
|
|
||||||
requestHandler.setCsrfRequestAttributeName(null);
|
|
||||||
http.csrf(
|
|
||||||
csrf ->
|
|
||||||
csrf.ignoringRequestMatchers(
|
|
||||||
request -> {
|
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
|
||||||
|
|
||||||
// If there's no API key, don't ignore CSRF
|
http.csrf(csrf -> csrf.disable());
|
||||||
// (return false)
|
|
||||||
if (apiKey == null || apiKey.trim().isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate API key using existing UserService
|
|
||||||
try {
|
|
||||||
Optional<User> user =
|
|
||||||
userService.getUserByApiKey(apiKey);
|
|
||||||
// If API key is valid, ignore CSRF (return
|
|
||||||
// true)
|
|
||||||
// If API key is invalid, don't ignore CSRF
|
|
||||||
// (return false)
|
|
||||||
return user.isPresent();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// If there's any error validating the API
|
|
||||||
// key, don't ignore CSRF
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.csrfTokenRepository(cookieRepo)
|
|
||||||
.csrfTokenRequestHandler(requestHandler));
|
|
||||||
}
|
|
||||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
http.sessionManagement(
|
http.sessionManagement(
|
||||||
@@ -143,154 +91,114 @@ public class SecurityConfiguration {
|
|||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.authenticationProvider(daoAuthenticationProvider());
|
http.formLogin(
|
||||||
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
|
formLogin ->
|
||||||
http.logout(
|
formLogin
|
||||||
logout ->
|
.loginPage("/login")
|
||||||
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
.successHandler(
|
||||||
.logoutSuccessHandler(
|
new CustomAuthenticationSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(applicationProperties))
|
loginAttemptService, userService))
|
||||||
.clearAuthentication(true)
|
.defaultSuccessUrl("/")
|
||||||
.invalidateHttpSession(true) // Invalidate session
|
.failureHandler(
|
||||||
.deleteCookies("JSESSIONID", "remember-me"));
|
new CustomAuthenticationFailureHandler(
|
||||||
http.rememberMe(
|
loginAttemptService, userService))
|
||||||
rememberMeConfigurer ->
|
.permitAll())
|
||||||
rememberMeConfigurer // Use the configurator directly
|
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
|
||||||
.key("uniqueAndSecret")
|
.logout(
|
||||||
.tokenRepository(persistentTokenRepository())
|
logout ->
|
||||||
.tokenValiditySeconds(1209600) // 2 weeks
|
logout.logoutRequestMatcher(
|
||||||
);
|
new AntPathRequestMatcher("/logout"))
|
||||||
http.authorizeHttpRequests(
|
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
|
||||||
authz ->
|
.invalidateHttpSession(true) // Invalidate session
|
||||||
authz.requestMatchers(
|
.deleteCookies("JSESSIONID", "remember-me"))
|
||||||
req -> {
|
.rememberMe(
|
||||||
String uri = req.getRequestURI();
|
rememberMeConfigurer ->
|
||||||
String contextPath = req.getContextPath();
|
rememberMeConfigurer // Use the configurator directly
|
||||||
|
.key("uniqueAndSecret")
|
||||||
|
.tokenRepository(persistentTokenRepository())
|
||||||
|
.tokenValiditySeconds(1209600) // 2 weeks
|
||||||
|
)
|
||||||
|
.authorizeHttpRequests(
|
||||||
|
authz ->
|
||||||
|
authz.requestMatchers(
|
||||||
|
req -> {
|
||||||
|
String uri = req.getRequestURI();
|
||||||
|
String contextPath = req.getContextPath();
|
||||||
|
|
||||||
// Remove the context path from the URI
|
// Remove the context path from the URI
|
||||||
String trimmedUri =
|
String trimmedUri =
|
||||||
uri.startsWith(contextPath)
|
uri.startsWith(contextPath)
|
||||||
? uri.substring(
|
? uri.substring(
|
||||||
contextPath.length())
|
contextPath
|
||||||
: uri;
|
.length())
|
||||||
|
: uri;
|
||||||
|
|
||||||
return trimmedUri.startsWith("/login")
|
return trimmedUri.startsWith("/login")
|
||||||
|| trimmedUri.startsWith("/oauth")
|
|| trimmedUri.startsWith("/oauth")
|
||||||
|| trimmedUri.startsWith("/saml2")
|
|| trimmedUri.endsWith(".svg")
|
||||||
|| trimmedUri.endsWith(".svg")
|
|| trimmedUri.startsWith(
|
||||||
|| trimmedUri.startsWith("/register")
|
"/register")
|
||||||
|| trimmedUri.startsWith("/error")
|
|| trimmedUri.startsWith("/error")
|
||||||
|| trimmedUri.startsWith("/images/")
|
|| trimmedUri.startsWith("/images/")
|
||||||
|| trimmedUri.startsWith("/public/")
|
|| trimmedUri.startsWith("/public/")
|
||||||
|| trimmedUri.startsWith("/css/")
|
|| trimmedUri.startsWith("/css/")
|
||||||
|| trimmedUri.startsWith("/fonts/")
|
|| trimmedUri.startsWith("/fonts/")
|
||||||
|| trimmedUri.startsWith("/js/")
|
|| trimmedUri.startsWith("/js/")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/api/v1/info/status");
|
"/api/v1/info/status");
|
||||||
})
|
})
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated());
|
.authenticated());
|
||||||
|
|
||||||
// Handle User/Password Logins
|
|
||||||
if (applicationProperties.getSecurity().isUserPass()) {
|
|
||||||
http.formLogin(
|
|
||||||
formLogin ->
|
|
||||||
formLogin
|
|
||||||
.loginPage("/login")
|
|
||||||
.successHandler(
|
|
||||||
new CustomAuthenticationSuccessHandler(
|
|
||||||
loginAttemptService, userService))
|
|
||||||
.failureHandler(
|
|
||||||
new CustomAuthenticationFailureHandler(
|
|
||||||
loginAttemptService, userService))
|
|
||||||
.defaultSuccessUrl("/")
|
|
||||||
.permitAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().isOauth2Activ()) {
|
if (applicationProperties.getSecurity().getOauth2() != null
|
||||||
|
&& applicationProperties.getSecurity().getOauth2().getEnabled()
|
||||||
|
&& !applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getLoginMethod()
|
||||||
|
.equalsIgnoreCase("normal")) {
|
||||||
|
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
oauth2.loginPage("/oauth2")
|
oauth2.loginPage("/oauth2")
|
||||||
/*
|
/*
|
||||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||||
is set as true, else login fails with an error message advising the same.
|
is set as true, else login fails with an error message advising the same.
|
||||||
*/
|
*/
|
||||||
.successHandler(
|
|
||||||
new CustomOAuth2AuthenticationSuccessHandler(
|
|
||||||
loginAttemptService,
|
|
||||||
applicationProperties,
|
|
||||||
userService))
|
|
||||||
.failureHandler(
|
|
||||||
new CustomOAuth2AuthenticationFailureHandler())
|
|
||||||
// Add existing Authorities from the database
|
|
||||||
.userInfoEndpoint(
|
|
||||||
userInfoEndpoint ->
|
|
||||||
userInfoEndpoint
|
|
||||||
.oidcUserService(
|
|
||||||
new CustomOAuth2UserService(
|
|
||||||
applicationProperties,
|
|
||||||
userService,
|
|
||||||
loginAttemptService))
|
|
||||||
.userAuthoritiesMapper(
|
|
||||||
userAuthoritiesMapper()))
|
|
||||||
.permitAll());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle SAML
|
|
||||||
if (applicationProperties.getSecurity().isSaml2Activ()
|
|
||||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
|
||||||
http.authenticationProvider(samlAuthenticationProvider());
|
|
||||||
http.saml2Login(
|
|
||||||
saml2 ->
|
|
||||||
saml2.loginPage("/saml2")
|
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomSaml2AuthenticationSuccessHandler(
|
new CustomOAuth2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
applicationProperties,
|
applicationProperties,
|
||||||
userService))
|
userService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomOAuth2AuthenticationFailureHandler())
|
||||||
.permitAll())
|
// Add existing Authorities from the database
|
||||||
.addFilterBefore(
|
.userInfoEndpoint(
|
||||||
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
|
userInfoEndpoint ->
|
||||||
|
userInfoEndpoint
|
||||||
|
.oidcUserService(
|
||||||
|
new CustomOAuth2UserService(
|
||||||
|
applicationProperties,
|
||||||
|
userService,
|
||||||
|
loginAttemptService))
|
||||||
|
.userAuthoritiesMapper(
|
||||||
|
userAuthoritiesMapper())))
|
||||||
|
.logout(
|
||||||
|
logout ->
|
||||||
|
logout.logoutSuccessHandler(
|
||||||
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
|
applicationProperties)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
http.csrf(csrf -> csrf.disable())
|
||||||
http.csrf(csrf -> csrf.disable());
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
} else {
|
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
|
||||||
new CsrfTokenRequestAttributeHandler();
|
|
||||||
requestHandler.setCsrfRequestAttributeName(null);
|
|
||||||
http.csrf(
|
|
||||||
csrf ->
|
|
||||||
csrf.csrfTokenRepository(cookieRepo)
|
|
||||||
.csrfTokenRequestHandler(requestHandler));
|
|
||||||
}
|
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public AuthenticationProvider samlAuthenticationProvider() {
|
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
|
||||||
new OpenSaml4AuthenticationProvider();
|
|
||||||
authenticationProvider.setResponseAuthenticationConverter(
|
|
||||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
|
||||||
return authenticationProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client Registration Repository for OAUTH2 OIDC Login
|
// Client Registration Repository for OAUTH2 OIDC Login
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(
|
||||||
@@ -306,7 +214,7 @@ public class SecurityConfiguration {
|
|||||||
keycloakClientRegistration().ifPresent(registrations::add);
|
keycloakClientRegistration().ifPresent(registrations::add);
|
||||||
|
|
||||||
if (registrations.isEmpty()) {
|
if (registrations.isEmpty()) {
|
||||||
log.error("At least one OAuth2 provider must be configured");
|
logger.error("At least one OAuth2 provider must be configured");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +275,6 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
@@ -422,52 +329,6 @@ public class SecurityConfiguration {
|
|||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
|
||||||
|
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
|
||||||
|
|
||||||
Resource certificateResource = samlConf.getSpCert();
|
|
||||||
|
|
||||||
Saml2X509Credential signingCredential =
|
|
||||||
new Saml2X509Credential(
|
|
||||||
CertificateUtils.readPrivateKey(privateKeyResource),
|
|
||||||
CertificateUtils.readCertificate(certificateResource),
|
|
||||||
Saml2X509CredentialType.SIGNING);
|
|
||||||
|
|
||||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
|
||||||
|
|
||||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
|
||||||
|
|
||||||
RelyingPartyRegistration rp =
|
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
|
||||||
.signingX509Credentials((c) -> c.add(signingCredential))
|
|
||||||
.assertingPartyDetails(
|
|
||||||
(details) ->
|
|
||||||
details.entityId(samlConf.getIdpIssuer())
|
|
||||||
.singleSignOnServiceLocation(
|
|
||||||
samlConf.getIdpSingleLoginUrl())
|
|
||||||
.verificationX509Credentials(
|
|
||||||
(c) -> c.add(verificationCredential))
|
|
||||||
.wantAuthnRequestsSigned(true))
|
|
||||||
.build();
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
|
||||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
|
||||||
provider.setUserDetailsService(userDetailsService);
|
|
||||||
provider.setPasswordEncoder(passwordEncoder());
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
@@ -525,14 +386,4 @@ public class SecurityConfiguration {
|
|||||||
public boolean activSecurity() {
|
public boolean activSecurity() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Only Dev test
|
|
||||||
// @Bean
|
|
||||||
// public WebSecurityCustomizer webSecurityCustomizer() {
|
|
||||||
// return (web) ->
|
|
||||||
// web.ignoring()
|
|
||||||
// .requestMatchers(
|
|
||||||
// "/css/**", "/images/**", "/js/**", "/**.svg",
|
|
||||||
// "/pdfjs-legacy/**");
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -22,7 +23,6 @@ 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.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
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;
|
import stirling.software.SPDF.model.User;
|
||||||
@@ -30,18 +30,13 @@ import stirling.software.SPDF.model.User;
|
|||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final UserService userService;
|
@Autowired @Lazy private UserService userService;
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
|
||||||
private final boolean loginEnabledValue;
|
|
||||||
|
|
||||||
public UserAuthenticationFilter(
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
@Lazy UserService userService,
|
|
||||||
SessionPersistentRegistry sessionPersistentRegistry,
|
@Autowired
|
||||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
@Qualifier("loginEnabled")
|
||||||
this.userService = userService;
|
public boolean loginEnabledValue;
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@@ -56,19 +51,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
// Check for session expiration (unsure if needed)
|
|
||||||
// if (authentication != null && authentication.isAuthenticated()) {
|
|
||||||
// String sessionId = request.getSession().getId();
|
|
||||||
// SessionInformation sessionInfo =
|
|
||||||
// sessionPersistentRegistry.getSessionInformation(sessionId);
|
|
||||||
//
|
|
||||||
// if (sessionInfo != null && sessionInfo.isExpired()) {
|
|
||||||
// SecurityContextHolder.clearContext();
|
|
||||||
// response.sendRedirect(request.getContextPath() + "/login?expired=true");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Check for API key in the request headers if no authentication exists
|
// Check for API key in the request headers if no authentication exists
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
@@ -112,9 +94,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.write(
|
||||||
"Authentication required. Please provide a X-API-KEY in request header.\n"
|
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected");
|
||||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
|
||||||
+ "Alternatively you can disable authentication if this is unexpected");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,8 +107,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
username = ((UserDetails) principal).getUsername();
|
username = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
username = ((OAuth2User) principal).getName();
|
username = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
username = (String) principal;
|
username = (String) principal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
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;
|
||||||
@@ -335,10 +334,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
OAuth2User oAuth2User = (OAuth2User) principal;
|
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||||
usernameP = oAuth2User.getName();
|
usernameP = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
CustomSaml2AuthenticatedPrincipal saml2User =
|
|
||||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
|
||||||
usernameP = saml2User.getName();
|
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
usernameP = (String) principal;
|
usernameP = (String) principal;
|
||||||
}
|
}
|
||||||
@@ -358,9 +353,4 @@ public class UserService implements UserServiceInterface {
|
|||||||
return principal.toString();
|
return principal.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getTotalUsersCount() {
|
|
||||||
return userRepository.count();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
}
|
}
|
||||||
log.error("OAuth2 Authentication error: " + errorCode);
|
log.error("OAuth2 Authentication error: " + errorCode);
|
||||||
log.error("OAuth2AuthenticationException", exception);
|
log.error("OAuth2AuthenticationException", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.error("Unhandled authentication exception", exception);
|
log.error("Unhandled authentication exception", exception);
|
||||||
|
|||||||
@@ -75,11 +75,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
if (userService.isUserDisabled(username)) {
|
|
||||||
getRedirectStrategy()
|
|
||||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& !userService.isAuthenticationTypeByUsername(
|
&& !userService.isAuthenticationTypeByUsername(
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.Provider;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
String param = "logout=true";
|
||||||
|
String registrationId = null;
|
||||||
|
String issuer = null;
|
||||||
|
String clientId = null;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
|
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
||||||
|
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Provider provider = oauth.getClient().get(registrationId);
|
||||||
|
issuer = provider.getIssuer();
|
||||||
|
clientId = provider.getClientId();
|
||||||
|
} catch (UnsupportedProviderException e) {
|
||||||
|
log.error(e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
|
issuer = oauth.getIssuer();
|
||||||
|
clientId = oauth.getClientId();
|
||||||
|
}
|
||||||
|
String errorMessage = "";
|
||||||
|
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||||
|
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||||
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
|
param = "error=" + sanitizeInput(errorMessage);
|
||||||
|
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
||||||
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
|
} 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;
|
||||||
|
|
||||||
|
switch (registrationId.toLowerCase()) {
|
||||||
|
case "keycloak":
|
||||||
|
// Add Keycloak specific logout URL if needed
|
||||||
|
String logoutUrl =
|
||||||
|
issuer
|
||||||
|
+ "/protocol/openid-connect/logout"
|
||||||
|
+ "?client_id="
|
||||||
|
+ clientId
|
||||||
|
+ "&post_logout_redirect_uri="
|
||||||
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
|
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
|
response.sendRedirect(logoutUrl);
|
||||||
|
break;
|
||||||
|
case "github":
|
||||||
|
// Add GitHub specific logout URL if needed
|
||||||
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
|
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
|
response.sendRedirect(githubLogoutUrl);
|
||||||
|
break;
|
||||||
|
case "google":
|
||||||
|
// Add Google specific logout URL if needed
|
||||||
|
// String googleLogoutUrl =
|
||||||
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
|
log.info("Google does not have a specific logout URL");
|
||||||
|
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
|
// break;
|
||||||
|
default:
|
||||||
|
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
|
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||||
|
response.sendRedirect(defaultRedirectUrl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitizeInput(String input) {
|
||||||
|
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
|
||||||
|
|
||||||
import org.bouncycastle.util.io.pem.PemObject;
|
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
|
|
||||||
public class CertificateUtils {
|
|
||||||
|
|
||||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
|
||||||
try (PemReader pemReader =
|
|
||||||
new PemReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
certificateResource.getInputStream(), StandardCharsets.UTF_8))) {
|
|
||||||
PemObject pemObject = pemReader.readPemObject();
|
|
||||||
byte[] decodedCert = pemObject.getContent();
|
|
||||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
|
||||||
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decodedCert));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
|
||||||
try (PemReader pemReader =
|
|
||||||
new PemReader(
|
|
||||||
new InputStreamReader(
|
|
||||||
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
|
||||||
PemObject pemObject = pemReader.readPemObject();
|
|
||||||
byte[] decodedKey = pemObject.getContent();
|
|
||||||
return (RSAPrivateKey)
|
|
||||||
KeyFactory.getInstance("RSA")
|
|
||||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
|
||||||
|
|
||||||
public class CustomSaml2AuthenticatedPrincipal
|
|
||||||
implements Saml2AuthenticatedPrincipal, Serializable {
|
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final Map<String, List<Object>> attributes;
|
|
||||||
private final String nameId;
|
|
||||||
private final List<String> sessionIndexes;
|
|
||||||
|
|
||||||
public CustomSaml2AuthenticatedPrincipal(
|
|
||||||
String name,
|
|
||||||
Map<String, List<Object>> attributes,
|
|
||||||
String nameId,
|
|
||||||
List<String> sessionIndexes) {
|
|
||||||
this.name = name;
|
|
||||||
this.attributes = attributes;
|
|
||||||
this.nameId = nameId;
|
|
||||||
this.sessionIndexes = sessionIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<String, List<Object>> getAttributes() {
|
|
||||||
return this.attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNameId() {
|
|
||||||
return this.nameId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getSessionIndexes() {
|
|
||||||
return this.sessionIndexes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.saml2.core.Saml2Error;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationFailure(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
AuthenticationException exception)
|
|
||||||
throws IOException, ServletException {
|
|
||||||
if (exception instanceof Saml2AuthenticationException) {
|
|
||||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
|
||||||
getRedirectStrategy()
|
|
||||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
|
||||||
} else if (exception instanceof ProviderNotFoundException) {
|
|
||||||
getRedirectStrategy()
|
|
||||||
.sendRedirect(
|
|
||||||
request,
|
|
||||||
response,
|
|
||||||
"/login?erroroauth=not_authentication_provider_found");
|
|
||||||
}
|
|
||||||
log.error("AuthenticationException: " + exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import jakarta.servlet.http.HttpSession;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class CustomSaml2AuthenticationSuccessHandler
|
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSuccess(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
|
|
||||||
Object principal = authentication.getPrincipal();
|
|
||||||
|
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
||||||
// Get the saved request
|
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
String contextPath = request.getContextPath();
|
|
||||||
SavedRequest savedRequest =
|
|
||||||
(session != null)
|
|
||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (savedRequest != null
|
|
||||||
&& !RequestUriUtils.isStaticResource(
|
|
||||||
contextPath, savedRequest.getRedirectUrl())) {
|
|
||||||
// Redirect to the original destination
|
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
|
||||||
} else {
|
|
||||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
|
||||||
if (session != null) {
|
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
|
||||||
}
|
|
||||||
throw new LockedException(
|
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
|
||||||
}
|
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
|
||||||
&& userService.hasPassword(username)
|
|
||||||
&& !userService.isAuthenticationTypeByUsername(
|
|
||||||
username, AuthenticationType.OAUTH2)
|
|
||||||
&& saml2.getAutoCreateUser()) {
|
|
||||||
response.sendRedirect(
|
|
||||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (saml2.getBlockRegistration()
|
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
|
||||||
response.sendRedirect(
|
|
||||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
|
|
||||||
response.sendRedirect(contextPath + "/");
|
|
||||||
return;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import org.opensaml.core.xml.XMLObject;
|
|
||||||
import org.opensaml.core.xml.schema.XSBoolean;
|
|
||||||
import org.opensaml.core.xml.schema.XSString;
|
|
||||||
import org.opensaml.saml.saml2.core.Assertion;
|
|
||||||
import org.opensaml.saml.saml2.core.Attribute;
|
|
||||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
|
||||||
import org.opensaml.saml.saml2.core.AuthnStatement;
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.model.User;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class CustomSaml2ResponseAuthenticationConverter
|
|
||||||
implements Converter<ResponseToken, Saml2Authentication> {
|
|
||||||
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
|
||||||
// Extract the assertion from the response
|
|
||||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
|
||||||
|
|
||||||
// Extract the NameID
|
|
||||||
String nameId = assertion.getSubject().getNameID().getValue();
|
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
|
|
||||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
|
||||||
if (userOpt.isPresent()) {
|
|
||||||
User user = userOpt.get();
|
|
||||||
if (user != null) {
|
|
||||||
simpleGrantedAuthority =
|
|
||||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the SessionIndexes
|
|
||||||
List<String> sessionIndexes = new ArrayList<>();
|
|
||||||
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
|
||||||
sessionIndexes.add(authnStatement.getSessionIndex());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the Attributes
|
|
||||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
|
||||||
|
|
||||||
// Create the custom principal
|
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
|
||||||
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
|
|
||||||
|
|
||||||
// Create the Saml2Authentication
|
|
||||||
return new Saml2Authentication(
|
|
||||||
principal,
|
|
||||||
responseToken.getToken().getSaml2Response(),
|
|
||||||
Collections.singletonList(simpleGrantedAuthority));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
|
||||||
Map<String, List<Object>> attributes = new HashMap<>();
|
|
||||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
|
||||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
|
||||||
String attributeName = attribute.getName();
|
|
||||||
List<Object> values = new ArrayList<>();
|
|
||||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
|
||||||
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
|
|
||||||
values.add(((XSString) xmlObject).getValue());
|
|
||||||
}
|
|
||||||
attributes.put(attributeName, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attributes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,19 +11,16 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomHttpSessionListener implements HttpSessionListener {
|
public class CustomHttpSessionListener implements HttpSessionListener {
|
||||||
|
|
||||||
private SessionPersistentRegistry sessionPersistentRegistry;
|
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) {
|
|
||||||
super();
|
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionCreated(HttpSessionEvent se) {}
|
public void sessionCreated(HttpSessionEvent se) {
|
||||||
|
log.info("Session created: " + se.getSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionDestroyed(HttpSessionEvent se) {
|
public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
|
log.info("Session destroyed: " + se.getSession().getId());
|
||||||
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -51,8 +50,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
principalName = ((UserDetails) principal).getUsername();
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
principalName = ((OAuth2User) principal).getName();
|
principalName = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
principalName = (String) principal;
|
principalName = (String) principal;
|
||||||
}
|
}
|
||||||
@@ -82,21 +79,11 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
principalName = ((UserDetails) principal).getUsername();
|
principalName = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
principalName = ((OAuth2User) principal).getName();
|
principalName = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
principalName = (String) principal;
|
principalName = (String) principal;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
// Clear old sessions for the principal (unsure if needed)
|
|
||||||
// List<SessionEntity> existingSessions =
|
|
||||||
// sessionRepository.findByPrincipalName(principalName);
|
|
||||||
// for (SessionEntity session : existingSessions) {
|
|
||||||
// session.setExpired(true);
|
|
||||||
// sessionRepository.save(session);
|
|
||||||
// }
|
|
||||||
|
|
||||||
SessionEntity sessionEntity = new SessionEntity();
|
SessionEntity sessionEntity = new SessionEntity();
|
||||||
sessionEntity.setSessionId(sessionId);
|
sessionEntity.setSessionId(sessionId);
|
||||||
sessionEntity.setPrincipalName(principalName);
|
sessionEntity.setPrincipalName(principalName);
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
|
||||||
import stirling.software.SPDF.service.LanguageService;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/js")
|
|
||||||
public class AdditionalLanguageJsController {
|
|
||||||
|
|
||||||
@Autowired private LanguageService languageService;
|
|
||||||
|
|
||||||
@Hidden
|
|
||||||
@GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript")
|
|
||||||
public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException {
|
|
||||||
List<String> supportedLanguages = languageService.getSupportedLanguages();
|
|
||||||
|
|
||||||
response.setContentType("application/javascript");
|
|
||||||
PrintWriter writer = response.getWriter();
|
|
||||||
|
|
||||||
// Erstelle das JavaScript dynamisch
|
|
||||||
writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";");
|
|
||||||
|
|
||||||
// Generiere die `getDetailedLanguageCode`-Funktion
|
|
||||||
writer.println(
|
|
||||||
"""
|
|
||||||
function getDetailedLanguageCode() {
|
|
||||||
const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
|
|
||||||
for (let lang of userLanguages) {
|
|
||||||
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
|
|
||||||
if (matchedLang) {
|
|
||||||
return matchedLang;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback
|
|
||||||
return "en_GB";
|
|
||||||
}
|
|
||||||
""");
|
|
||||||
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hilfsfunktion zum Konvertieren der Liste in ein JSON-Array
|
|
||||||
private String toJsonArray(List<String> list) {
|
|
||||||
StringBuilder jsonArray = new StringBuilder("[");
|
|
||||||
for (int i = 0; i < list.size(); i++) {
|
|
||||||
jsonArray.append("\"").append(list.get(i)).append("\"");
|
|
||||||
if (i < list.size() - 1) {
|
|
||||||
jsonArray.append(",");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonArray.append("]");
|
|
||||||
return jsonArray.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.service.PostHogService;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -37,13 +36,9 @@ public class CropController {
|
|||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final PostHogService postHogService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public CropController(
|
public CropController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
this.postHogService = postHogService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
@@ -22,7 +21,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/general")
|
@RequestMapping("/api/v1/general")
|
||||||
@Tag(name = "General", description = "General APIs")
|
|
||||||
public class PdfImageRemovalController {
|
public class PdfImageRemovalController {
|
||||||
|
|
||||||
// Service for removing images from PDFs
|
// Service for removing images from PDFs
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public class ScalePagesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
|
private PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
|
||||||
if ("KEEP".equals(targetPDRectangle)) {
|
if (targetPDRectangle.equals("KEEP")) {
|
||||||
if (sourceDocument.getNumberOfPages() == 0) {
|
if (sourceDocument.getNumberOfPages() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
@Tag(name = "Settings", description = "Settings APIs")
|
|
||||||
@RequestMapping("/api/v1/settings")
|
|
||||||
@Hidden
|
|
||||||
public class SettingsController {
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@PostMapping("/update-enable-analytics")
|
|
||||||
@Hidden
|
|
||||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
|
||||||
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
|
||||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
|
||||||
.body(
|
|
||||||
"Setting has already been set, To adjust please edit /config/settings.yml");
|
|
||||||
}
|
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
|
||||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
|
||||||
|
|
||||||
return ResponseEntity.ok("Updated");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -60,6 +60,8 @@ public class SplitPDFController {
|
|||||||
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||||
|
System.out.println(
|
||||||
|
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
if (!pageNumbers.contains(totalPages - 1)) {
|
if (!pageNumbers.contains(totalPages - 1)) {
|
||||||
// Create a mutable ArrayList so we can add to it
|
// Create a mutable ArrayList so we can add to it
|
||||||
pageNumbers = new ArrayList<>(pageNumbers);
|
pageNumbers = new ArrayList<>(pageNumbers);
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import stirling.software.SPDF.config.PdfMetadataService;
|
||||||
import stirling.software.SPDF.model.PdfMetadata;
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||||
import stirling.software.SPDF.service.PdfMetadataService;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -67,6 +67,15 @@ public class SplitPdfByChaptersController {
|
|||||||
}
|
}
|
||||||
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
|
// checks if the document is encrypted by an empty user password
|
||||||
|
if (sourceDocument.isEncrypted()) {
|
||||||
|
try {
|
||||||
|
sourceDocument.setAllSecurityToBeRemoved(true);
|
||||||
|
logger.info("Removing security from the source document ");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Cannot decrypt the pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
||||||
|
|
||||||
if (outline == null) {
|
if (outline == null) {
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ 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 lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
@@ -42,7 +40,6 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
|||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "User", description = "User APIs")
|
@Tag(name = "User", description = "User APIs")
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
@Slf4j
|
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
@@ -194,11 +191,13 @@ public class UserController {
|
|||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
Map<String, String> updates = new HashMap<>();
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
|
||||||
|
System.out.println("Received parameter map: " + paramMap);
|
||||||
|
|
||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("Processed updates: " + updates);
|
System.out.println("Processed updates: " + updates);
|
||||||
|
|
||||||
// Assuming you have a method in userService to update the settings for a user
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
userService.updateUserSettings(principal.getName(), updates);
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
@@ -210,7 +209,7 @@ public class UserController {
|
|||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam(name = "username", required = true) String username,
|
@RequestParam(name = "username", required = true) String username,
|
||||||
@RequestParam(name = "password", required = false) String password,
|
@RequestParam(name = "password", required = true) String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
@RequestParam(name = "authType") String authType,
|
@RequestParam(name = "authType") String authType,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
@@ -337,8 +336,6 @@ public class UserController {
|
|||||||
userNameP = ((UserDetails) principal).getUsername();
|
userNameP = ((UserDetails) principal).getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
userNameP = ((OAuth2User) principal).getName();
|
userNameP = ((OAuth2User) principal).getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
userNameP = (String) principal;
|
userNameP = (String) principal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -15,6 +14,7 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
|||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
// Disabled for now
|
||||||
// @RestController
|
// @RestController
|
||||||
// @Tag(name = "Convert", description = "Convert APIs")
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
// @RequestMapping("/api/v1/convert")
|
// @RequestMapping("/api/v1/convert")
|
||||||
@@ -24,7 +24,7 @@ public class ConvertBookToPDFController {
|
|||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
@Autowired
|
// @Autowired
|
||||||
public ConvertBookToPDFController(
|
public ConvertBookToPDFController(
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
||||||
@@ -66,8 +66,6 @@ public class ConvertBookToPDFController {
|
|||||||
}
|
}
|
||||||
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
|
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
+ ".pdf"; // Remove file extension and append .pdf
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
|||||||
@@ -1,39 +1,27 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
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;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
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 stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
// Disabled for now
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
// @RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
// @RequestMapping("/api/v1/convert")
|
||||||
public class ConvertHtmlToPDF {
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
// @Autowired
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ConvertHtmlToPDF(
|
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -61,8 +49,6 @@ public class ConvertHtmlToPDF {
|
|||||||
originalFilename,
|
originalFilename,
|
||||||
bookAndHtmlFormatsInstalled);
|
bookAndHtmlFormatsInstalled);
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
|
||||||
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
+ ".pdf"; // Remove file extension and append .pdf
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class ConvertImgPDFController {
|
|||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
|
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
|
||||||
colorTypeResult,
|
colorTypeResult,
|
||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
@@ -90,9 +90,9 @@ public class ConvertImgPDFController {
|
|||||||
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 ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
|
||||||
throw new IOException("Python is not installed. Required for WebP conversion.");
|
throw new IOException("Python is not installed. Required for WebP conversion.");
|
||||||
} else if ("webp".equalsIgnoreCase(imageFormat)
|
} else if (imageFormat.equalsIgnoreCase("webp")
|
||||||
&& CheckProgramInstall.isPythonAvailable()) {
|
&& CheckProgramInstall.isPythonAvailable()) {
|
||||||
// Write the output stream to a temp file
|
// Write the output stream to a temp file
|
||||||
Path tempFile = Files.createTempFile("temp_png", ".png");
|
Path tempFile = Files.createTempFile("temp_png", ".png");
|
||||||
|
|||||||
@@ -10,40 +10,28 @@ import org.commonmark.node.Node;
|
|||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
import org.commonmark.renderer.html.AttributeProvider;
|
import org.commonmark.renderer.html.AttributeProvider;
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
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;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
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 stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
import stirling.software.SPDF.utils.FileToPdf;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
// Disabled for now
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
// @RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
|
// @RequestMapping("/api/v1/convert")
|
||||||
public class ConvertMarkdownToPdf {
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
// @Autowired
|
||||||
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ConvertMarkdownToPdf(
|
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -82,7 +70,7 @@ public class ConvertMarkdownToPdf {
|
|||||||
htmlContent.getBytes(),
|
htmlContent.getBytes(),
|
||||||
"converted.html",
|
"converted.html",
|
||||||
bookAndHtmlFormatsInstalled);
|
bookAndHtmlFormatsInstalled);
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
originalFilename.replaceFirst("[.][^.]+$", "")
|
||||||
+ ".pdf"; // Remove file extension and append .pdf
|
+ ".pdf"; // Remove file extension and append .pdf
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
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,12 +20,13 @@ import stirling.software.SPDF.utils.ProcessExecutor;
|
|||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
// Disabled for now
|
||||||
// @RestController
|
// @RestController
|
||||||
// @Tag(name = "Convert", description = "Convert APIs")
|
// @Tag(name = "Convert", description = "Convert APIs")
|
||||||
// @RequestMapping("/api/v1/convert")
|
// @RequestMapping("/api/v1/convert")
|
||||||
public class ConvertPDFToBookController {
|
public class ConvertPDFToBookController {
|
||||||
|
|
||||||
@Autowired
|
// @Autowired
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled")
|
@Qualifier("bookAndHtmlFormatsInstalled")
|
||||||
private boolean bookAndHtmlFormatsInstalled;
|
private boolean bookAndHtmlFormatsInstalled;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
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;
|
||||||
@@ -22,6 +29,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.converters.PdfToPdfARequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||||
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -33,6 +41,13 @@ public class ConvertPDFToPDFA {
|
|||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
|
private static final Logger logger = LoggerFactory.getLogger(ConvertPDFToPDFA.class);
|
||||||
|
|
||||||
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ConvertPDFToPDFA(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||||
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
@PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
@@ -46,7 +61,32 @@ public class ConvertPDFToPDFA {
|
|||||||
// Convert MultipartFile to byte[]
|
// Convert MultipartFile to byte[]
|
||||||
byte[] pdfBytes = inputFile.getBytes();
|
byte[] pdfBytes = inputFile.getBytes();
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
// Load the PDF document
|
||||||
|
PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||||
|
|
||||||
|
// Get the document catalog
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
|
||||||
|
// Get the AcroForm
|
||||||
|
PDAcroForm acroForm = catalog.getAcroForm();
|
||||||
|
if (acroForm != null) {
|
||||||
|
// Remove signature fields safely
|
||||||
|
List<PDField> fieldsToRemove =
|
||||||
|
acroForm.getFields().stream()
|
||||||
|
.filter(field -> field instanceof PDSignatureField)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
|
acroForm.flatten(fieldsToRemove, false);
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
pdfBytes = baos.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
// Save the uploaded (and possibly modified) file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
||||||
outputStream.write(pdfBytes);
|
outputStream.write(pdfBytes);
|
||||||
@@ -55,37 +95,28 @@ public class ConvertPDFToPDFA {
|
|||||||
// Prepare the output file path
|
// Prepare the output file path
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
|
|
||||||
// Prepare the ghostscript command
|
// Prepare the OCRmyPDF command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("gs");
|
command.add("ocrmypdf");
|
||||||
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
|
command.add("--skip-text");
|
||||||
command.add("-dNOPAUSE");
|
command.add("--tesseract-timeout=0");
|
||||||
command.add("-dBATCH");
|
command.add("--output-type");
|
||||||
command.add("-sColorConversionStrategy=sRGB");
|
command.add(outputFormat.toString());
|
||||||
command.add("-sDEVICE=pdfwrite");
|
|
||||||
command.add("-dPDFACompatibilityPolicy=2");
|
|
||||||
command.add("-o");
|
|
||||||
command.add(tempOutputFile.toString());
|
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
if (returnCode.getRc() != 0) {
|
|
||||||
logger.info(
|
|
||||||
outputFormat + " conversion failed with return code: " + returnCode.getRc());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
|
PDDocument doc = pdfDocumentFactory.load(tempOutputFile.toFile());
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_PDFA.pdf";
|
+ "_PDFA.pdf";
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(doc, outputFilename);
|
||||||
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.deleteIfExists(tempInputFile);
|
Files.deleteIfExists(tempInputFile);
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ public class ExtractImagesController {
|
|||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String format = request.getFormat();
|
||||||
boolean allowDuplicates = request.isAllowDuplicates();
|
boolean allowDuplicates = request.isAllowDuplicates();
|
||||||
|
System.out.println(
|
||||||
|
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
|
// Determine if multithreading should be used based on PDF size or number of pages
|
||||||
@@ -88,35 +90,22 @@ public class ExtractImagesController {
|
|||||||
// Iterate over each page
|
// Iterate over each page
|
||||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
||||||
PDPage page = document.getPage(pgNum);
|
PDPage page = document.getPage(pgNum);
|
||||||
|
int pageNum = document.getPages().indexOf(page) + 1;
|
||||||
|
// Submit a task for processing each page
|
||||||
Future<Void> future =
|
Future<Void> future =
|
||||||
executor.submit(
|
executor.submit(
|
||||||
() -> {
|
() -> {
|
||||||
// Use the page number directly from the iterator, so no need to
|
extractImagesFromPage(
|
||||||
// calculate manually
|
page,
|
||||||
int pageNum = document.getPages().indexOf(page) + 1;
|
format,
|
||||||
|
filename,
|
||||||
try {
|
pageNum,
|
||||||
// Call the image extraction method for each page
|
processedImages,
|
||||||
extractImagesFromPage(
|
zos,
|
||||||
page,
|
allowDuplicates);
|
||||||
format,
|
return null;
|
||||||
filename,
|
|
||||||
pageNum,
|
|
||||||
processedImages,
|
|
||||||
zos,
|
|
||||||
allowDuplicates);
|
|
||||||
} catch (IOException e) {
|
|
||||||
// Log the error and continue processing other pages
|
|
||||||
logger.error(
|
|
||||||
"Error extracting images from page {}: {}",
|
|
||||||
pageNum,
|
|
||||||
e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // Callable requires a return type
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add the Future object to the list to track completion
|
|
||||||
futures.add(future);
|
futures.add(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
@Slf4j
|
|
||||||
public class PrintFileController {
|
public class PrintFileController {
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -61,7 +59,7 @@ public class PrintFileController {
|
|||||||
new IllegalArgumentException(
|
new IllegalArgumentException(
|
||||||
"No matching printer found"));
|
"No matching printer found"));
|
||||||
|
|
||||||
log.info("Selected Printer: " + selectedService.getName());
|
System.out.println("Selected Printer: " + selectedService.getName());
|
||||||
|
|
||||||
if ("application/pdf".equals(contentType)) {
|
if ("application/pdf".equals(contentType)) {
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.InputStreamResource;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
|
|
||||||
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/misc")
|
|
||||||
public class ReplaceAndInvertColorController {
|
|
||||||
|
|
||||||
private ReplaceAndInvertColorService replaceAndInvertColorService;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ReplaceAndInvertColorController(
|
|
||||||
ReplaceAndInvertColorService replaceAndInvertColorService) {
|
|
||||||
this.replaceAndInvertColorService = replaceAndInvertColorService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
|
|
||||||
@Operation(
|
|
||||||
summary = "Replace-Invert Color PDF",
|
|
||||||
description =
|
|
||||||
"This endpoint accepts a PDF file and option of invert all colors or replace text and background colors. Input:PDF Output:PDF Type:SISO")
|
|
||||||
public ResponseEntity<InputStreamResource> replaceAndInvertColor(
|
|
||||||
@ModelAttribute ReplaceAndInvertColorRequest replaceAndInvertColorRequest)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
InputStreamResource resource =
|
|
||||||
replaceAndInvertColorService.replaceAndInvertColor(
|
|
||||||
replaceAndInvertColorRequest.getFileInput(),
|
|
||||||
replaceAndInvertColorRequest.getReplaceAndInvertOption(),
|
|
||||||
replaceAndInvertColorRequest.getHighContrastColorCombination(),
|
|
||||||
replaceAndInvertColorRequest.getBackGroundColor(),
|
|
||||||
replaceAndInvertColorRequest.getTextColor());
|
|
||||||
|
|
||||||
// Return the modified PDF as a downloadable file
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=inverted.pdf")
|
|
||||||
.contentType(MediaType.APPLICATION_PDF)
|
|
||||||
.body(resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,4 @@ public interface UserServiceInterface {
|
|||||||
String getApiKeyForUser(String username);
|
String getApiKeyForUser(String username);
|
||||||
|
|
||||||
String getCurrentUsername();
|
String getCurrentUsername();
|
||||||
|
|
||||||
long getTotalUsersCount();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.security.KeyStore;
|
import java.security.KeyStore;
|
||||||
import java.security.KeyStoreException;
|
import java.security.KeyStoreException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
@@ -18,39 +14,12 @@ import java.security.UnrecoverableKeyException;
|
|||||||
import java.security.cert.Certificate;
|
import java.security.cert.Certificate;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.apache.pdfbox.examples.signature.CreateSignatureBase;
|
import org.apache.pdfbox.examples.signature.CreateSignatureBase;
|
||||||
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.PDResources;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.blend.BlendMode;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|
||||||
import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
|
||||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
|
||||||
import org.apache.pdfbox.util.Matrix;
|
|
||||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||||
import org.bouncycastle.asn1.x500.RDN;
|
|
||||||
import org.bouncycastle.asn1.x500.X500Name;
|
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
|
||||||
import org.bouncycastle.asn1.x500.style.IETFUtils;
|
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
import org.bouncycastle.openssl.PEMDecryptorProvider;
|
import org.bouncycastle.openssl.PEMDecryptorProvider;
|
||||||
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
|
||||||
@@ -66,7 +35,6 @@ import org.bouncycastle.pkcs.PKCSException;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
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;
|
||||||
@@ -94,8 +62,6 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CreateSignature extends CreateSignatureBase {
|
class CreateSignature extends CreateSignatureBase {
|
||||||
File logoFile;
|
|
||||||
|
|
||||||
public CreateSignature(KeyStore keystore, char[] pin)
|
public CreateSignature(KeyStore keystore, char[] pin)
|
||||||
throws KeyStoreException,
|
throws KeyStoreException,
|
||||||
UnrecoverableKeyException,
|
UnrecoverableKeyException,
|
||||||
@@ -103,101 +69,6 @@ public class CertSignController {
|
|||||||
IOException,
|
IOException,
|
||||||
CertificateException {
|
CertificateException {
|
||||||
super(keystore, pin);
|
super(keystore, pin);
|
||||||
ClassPathResource resource = new ClassPathResource("static/images/signature.png");
|
|
||||||
try (InputStream is = resource.getInputStream()) {
|
|
||||||
logoFile = Files.createTempFile("signature", ".png").toFile();
|
|
||||||
FileUtils.copyInputStreamToFile(is, logoFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Failed to load image signature file");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream createVisibleSignature(
|
|
||||||
PDDocument srcDoc, PDSignature signature, Integer pageNumber, Boolean showLogo)
|
|
||||||
throws IOException {
|
|
||||||
// modified from org.apache.pdfbox.examples.signature.CreateVisibleSignature2
|
|
||||||
try (PDDocument doc = new PDDocument()) {
|
|
||||||
PDPage page = new PDPage(srcDoc.getPage(pageNumber).getMediaBox());
|
|
||||||
doc.addPage(page);
|
|
||||||
PDAcroForm acroForm = new PDAcroForm(doc);
|
|
||||||
doc.getDocumentCatalog().setAcroForm(acroForm);
|
|
||||||
PDSignatureField signatureField = new PDSignatureField(acroForm);
|
|
||||||
PDAnnotationWidget widget = signatureField.getWidgets().get(0);
|
|
||||||
List<PDField> acroFormFields = acroForm.getFields();
|
|
||||||
acroForm.setSignaturesExist(true);
|
|
||||||
acroForm.setAppendOnly(true);
|
|
||||||
acroForm.getCOSObject().setDirect(true);
|
|
||||||
acroFormFields.add(signatureField);
|
|
||||||
|
|
||||||
PDRectangle rect = new PDRectangle(0, 0, 200, 50);
|
|
||||||
|
|
||||||
widget.setRectangle(rect);
|
|
||||||
|
|
||||||
// from PDVisualSigBuilder.createHolderForm()
|
|
||||||
PDStream stream = new PDStream(doc);
|
|
||||||
PDFormXObject form = new PDFormXObject(stream);
|
|
||||||
PDResources res = new PDResources();
|
|
||||||
form.setResources(res);
|
|
||||||
form.setFormType(1);
|
|
||||||
PDRectangle bbox = new PDRectangle(rect.getWidth(), rect.getHeight());
|
|
||||||
float height = bbox.getHeight();
|
|
||||||
form.setBBox(bbox);
|
|
||||||
PDFont font = new PDType1Font(FontName.TIMES_BOLD);
|
|
||||||
|
|
||||||
// from PDVisualSigBuilder.createAppearanceDictionary()
|
|
||||||
PDAppearanceDictionary appearance = new PDAppearanceDictionary();
|
|
||||||
appearance.getCOSObject().setDirect(true);
|
|
||||||
PDAppearanceStream appearanceStream = new PDAppearanceStream(form.getCOSObject());
|
|
||||||
appearance.setNormalAppearance(appearanceStream);
|
|
||||||
widget.setAppearance(appearance);
|
|
||||||
|
|
||||||
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
|
|
||||||
if (showLogo) {
|
|
||||||
cs.saveGraphicsState();
|
|
||||||
PDExtendedGraphicsState extState = new PDExtendedGraphicsState();
|
|
||||||
extState.setBlendMode(BlendMode.MULTIPLY);
|
|
||||||
extState.setNonStrokingAlphaConstant(0.5f);
|
|
||||||
cs.setGraphicsStateParameters(extState);
|
|
||||||
cs.transform(Matrix.getScaleInstance(0.08f, 0.08f));
|
|
||||||
PDImageXObject img =
|
|
||||||
PDImageXObject.createFromFileByExtension(logoFile, doc);
|
|
||||||
cs.drawImage(img, 100, 0);
|
|
||||||
cs.restoreGraphicsState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// show text
|
|
||||||
float fontSize = 10;
|
|
||||||
float leading = fontSize * 1.5f;
|
|
||||||
cs.beginText();
|
|
||||||
cs.setFont(font, fontSize);
|
|
||||||
cs.setNonStrokingColor(Color.black);
|
|
||||||
cs.newLineAtOffset(fontSize, height - leading);
|
|
||||||
cs.setLeading(leading);
|
|
||||||
|
|
||||||
X509Certificate cert = (X509Certificate) getCertificateChain()[0];
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/2914521/
|
|
||||||
X500Name x500Name = new X500Name(cert.getSubjectX500Principal().getName());
|
|
||||||
RDN cn = x500Name.getRDNs(BCStyle.CN)[0];
|
|
||||||
String name = IETFUtils.valueToString(cn.getFirst().getValue());
|
|
||||||
|
|
||||||
String date = signature.getSignDate().getTime().toString();
|
|
||||||
String reason = signature.getReason();
|
|
||||||
|
|
||||||
cs.showText("Signed by " + name);
|
|
||||||
cs.newLine();
|
|
||||||
cs.showText(date);
|
|
||||||
cs.newLine();
|
|
||||||
cs.showText(reason);
|
|
||||||
|
|
||||||
cs.endText();
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
doc.save(baos);
|
|
||||||
return new ByteArrayInputStream(baos.toByteArray());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,8 +97,7 @@ public class CertSignController {
|
|||||||
String reason = request.getReason();
|
String reason = request.getReason();
|
||||||
String location = request.getLocation();
|
String location = request.getLocation();
|
||||||
String name = request.getName();
|
String name = request.getName();
|
||||||
Integer pageNumber = request.getPageNumber() - 1;
|
Integer pageNumber = request.getPageNumber();
|
||||||
Boolean showLogo = request.isShowLogo();
|
|
||||||
|
|
||||||
if (certType == null) {
|
if (certType == null) {
|
||||||
throw new IllegalArgumentException("Cert type must be provided");
|
throw new IllegalArgumentException("Cert type must be provided");
|
||||||
@@ -256,19 +126,11 @@ public class CertSignController {
|
|||||||
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
throw new IllegalArgumentException("Invalid cert type: " + certType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: page number
|
||||||
|
|
||||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
sign(
|
sign(pdfDocumentFactory, pdf.getBytes(), baos, createSignature, name, location, reason);
|
||||||
pdfDocumentFactory,
|
|
||||||
pdf.getBytes(),
|
|
||||||
baos,
|
|
||||||
createSignature,
|
|
||||||
showSignature,
|
|
||||||
pageNumber,
|
|
||||||
name,
|
|
||||||
location,
|
|
||||||
reason,
|
|
||||||
showLogo);
|
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.boasToWebResponse(
|
||||||
baos,
|
baos,
|
||||||
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
@@ -280,12 +142,9 @@ public class CertSignController {
|
|||||||
byte[] input,
|
byte[] input,
|
||||||
OutputStream output,
|
OutputStream output,
|
||||||
CreateSignature instance,
|
CreateSignature instance,
|
||||||
Boolean showSignature,
|
|
||||||
Integer pageNumber,
|
|
||||||
String name,
|
String name,
|
||||||
String location,
|
String location,
|
||||||
String reason,
|
String reason) {
|
||||||
Boolean showLogo) {
|
|
||||||
try (PDDocument doc = pdfDocumentFactory.load(input)) {
|
try (PDDocument doc = pdfDocumentFactory.load(input)) {
|
||||||
PDSignature signature = new PDSignature();
|
PDSignature signature = new PDSignature();
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||||
@@ -295,17 +154,7 @@ public class CertSignController {
|
|||||||
signature.setReason(reason);
|
signature.setReason(reason);
|
||||||
signature.setSignDate(Calendar.getInstance());
|
signature.setSignDate(Calendar.getInstance());
|
||||||
|
|
||||||
if (showSignature) {
|
doc.addSignature(signature, instance);
|
||||||
SignatureOptions signatureOptions = new SignatureOptions();
|
|
||||||
signatureOptions.setVisualSignature(
|
|
||||||
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
|
|
||||||
signatureOptions.setPage(pageNumber);
|
|
||||||
|
|
||||||
doc.addSignature(signature, instance, signatureOptions);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
doc.addSignature(signature, instance);
|
|
||||||
}
|
|
||||||
doc.saveIncremental(output);
|
doc.saveIncremental(output);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("exception", e);
|
logger.error("exception", e);
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ public class RedactController {
|
|||||||
float customPadding = request.getCustomPadding();
|
float customPadding = request.getCustomPadding();
|
||||||
boolean convertPDFToImage = request.isConvertPDFToImage();
|
boolean convertPDFToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
|
System.out.println(listOfTextString);
|
||||||
String[] listOfText = listOfTextString.split("\n");
|
String[] listOfText = listOfTextString.split("\n");
|
||||||
PDDocument document = pdfDocumentFactory.load(file);
|
PDDocument document = pdfDocumentFactory.load(file);
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ public class RedactController {
|
|||||||
|
|
||||||
for (String text : listOfText) {
|
for (String text : listOfText) {
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
|
System.out.println(text);
|
||||||
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
||||||
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
||||||
redactFoundText(document, foundTexts, customPadding, redactColor);
|
redactFoundText(document, foundTexts, customPadding, redactColor);
|
||||||
|
|||||||
@@ -21,13 +21,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.*;
|
import stirling.software.SPDF.model.*;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
|
||||||
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.ApplicationProperties.Security.SAML2;
|
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
@@ -54,54 +51,38 @@ public class AccountWebController {
|
|||||||
|
|
||||||
Map<String, String> providerList = new HashMap<>();
|
Map<String, String> providerList = new HashMap<>();
|
||||||
|
|
||||||
Security securityProps = applicationProperties.getSecurity();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
|
||||||
OAUTH2 oauth = securityProps.getOauth2();
|
|
||||||
if (oauth != null) {
|
if (oauth != null) {
|
||||||
if (oauth.getEnabled()) {
|
if (oauth.isSettingsValid()) {
|
||||||
if (oauth.isSettingsValid()) {
|
providerList.put("oidc", oauth.getProvider());
|
||||||
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client != null) {
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
if (google.isSettingsValid()) {
|
||||||
|
providerList.put(google.getName(), google.getClientName());
|
||||||
}
|
}
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client != null) {
|
|
||||||
GoogleProvider google = client.getGoogle();
|
|
||||||
if (google.isSettingsValid()) {
|
|
||||||
providerList.put(
|
|
||||||
"/oauth2/authorization/" + google.getName(),
|
|
||||||
google.getClientName());
|
|
||||||
}
|
|
||||||
|
|
||||||
GithubProvider github = client.getGithub();
|
GithubProvider github = client.getGithub();
|
||||||
if (github.isSettingsValid()) {
|
if (github.isSettingsValid()) {
|
||||||
providerList.put(
|
providerList.put(github.getName(), github.getClientName());
|
||||||
"/oauth2/authorization/" + github.getName(),
|
}
|
||||||
github.getClientName());
|
|
||||||
}
|
|
||||||
|
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
if (keycloak.isSettingsValid()) {
|
if (keycloak.isSettingsValid()) {
|
||||||
providerList.put(
|
providerList.put(keycloak.getName(), keycloak.getClientName());
|
||||||
"/oauth2/authorization/" + keycloak.getName(),
|
|
||||||
keycloak.getClientName());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SAML2 saml2 = securityProps.getSaml2();
|
|
||||||
if (securityProps.isSaml2Activ()
|
|
||||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
|
||||||
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
|
|
||||||
}
|
|
||||||
// Remove any null keys/values from the providerList
|
// Remove any null keys/values from the providerList
|
||||||
providerList
|
providerList
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||||
model.addAttribute("providerlist", providerList);
|
model.addAttribute("providerlist", providerList);
|
||||||
|
|
||||||
model.addAttribute("loginMethod", securityProps.getLoginMethod());
|
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
|
||||||
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
|
model.addAttribute(
|
||||||
model.addAttribute("altLogin", altLogin);
|
"oAuth2Enabled", applicationProperties.getSecurity().getOauth2().getEnabled());
|
||||||
|
|
||||||
model.addAttribute("currentPage", "login");
|
model.addAttribute("currentPage", "login");
|
||||||
|
|
||||||
@@ -164,17 +145,6 @@ public class AccountWebController {
|
|||||||
case "userIsDisabled":
|
case "userIsDisabled":
|
||||||
erroroauth = "login.userIsDisabled";
|
erroroauth = "login.userIsDisabled";
|
||||||
break;
|
break;
|
||||||
case "invalid_destination":
|
|
||||||
erroroauth = "login.invalid_destination";
|
|
||||||
break;
|
|
||||||
// Valid InResponseTo was not available from the validation context, unable to
|
|
||||||
// evaluate
|
|
||||||
case "invalid_in_response_to":
|
|
||||||
erroroauth = "login.invalid_in_response_to";
|
|
||||||
break;
|
|
||||||
case "not_authentication_provider_found":
|
|
||||||
erroroauth = "login.not_authentication_provider_found";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -379,17 +349,6 @@ public class AccountWebController {
|
|||||||
// Add oAuth2 Login attributes to the model
|
// Add oAuth2 Login attributes to the model
|
||||||
model.addAttribute("oAuth2Login", true);
|
model.addAttribute("oAuth2Login", true);
|
||||||
}
|
}
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
||||||
// Cast the principal object to OAuth2User
|
|
||||||
CustomSaml2AuthenticatedPrincipal userDetails =
|
|
||||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
|
||||||
|
|
||||||
// Retrieve username and other attributes
|
|
||||||
username = userDetails.getName();
|
|
||||||
// Add oAuth2 Login attributes to the model
|
|
||||||
model.addAttribute("oAuth2Login", true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user =
|
Optional<User> user =
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import stirling.software.SPDF.utils.CheckProgramInstall;
|
|||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConverterWebController {
|
public class ConverterWebController {
|
||||||
|
|
||||||
@ConditionalOnExpression("${bookAndHtmlFormatsInstalled}")
|
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
||||||
@GetMapping("/book-to-pdf")
|
@GetMapping("/book-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertBookToPdfForm(Model model) {
|
public String convertBookToPdfForm(Model model) {
|
||||||
@@ -60,7 +60,7 @@ public class ConverterWebController {
|
|||||||
|
|
||||||
// PDF TO......
|
// PDF TO......
|
||||||
|
|
||||||
@ConditionalOnExpression("${bookAndHtmlFormatsInstalled}")
|
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
||||||
@GetMapping("/pdf-to-book")
|
@GetMapping("/pdf-to-book")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertPdfToBookForm(Model model) {
|
public String convertPdfToBookForm(Model model) {
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
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.controller.api.pipeline.UserServiceInterface;
|
|
||||||
import stirling.software.SPDF.model.SignatureFile;
|
|
||||||
import stirling.software.SPDF.service.SignatureService;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "General", description = "General APIs")
|
@Tag(name = "General", description = "General APIs")
|
||||||
public class GeneralWebController {
|
public class GeneralWebController {
|
||||||
@@ -112,13 +108,6 @@ public class GeneralWebController {
|
|||||||
return "split-pdf-by-sections";
|
return "split-pdf-by-sections";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/split-pdf-by-chapters")
|
|
||||||
@Hidden
|
|
||||||
public String splitPdfByChapters(Model model) {
|
|
||||||
model.addAttribute("currentPage", "split-pdf-by-chapters");
|
|
||||||
return "split-pdf-by-chapters";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/view-pdf")
|
@GetMapping("/view-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String ViewPdfForm2(Model model) {
|
public String ViewPdfForm2(Model model) {
|
||||||
@@ -175,28 +164,11 @@ public class GeneralWebController {
|
|||||||
return "split-pdfs";
|
return "split-pdfs";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
|
|
||||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
|
||||||
|
|
||||||
@Autowired private SignatureService signatureService;
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private UserServiceInterface userService;
|
|
||||||
|
|
||||||
@GetMapping("/sign")
|
@GetMapping("/sign")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String signForm(Model model) {
|
public String signForm(Model model) {
|
||||||
String username = "";
|
|
||||||
if (userService != null) {
|
|
||||||
username = userService.getCurrentUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get signatures from both personal and ALL_USERS folders
|
|
||||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
|
||||||
|
|
||||||
model.addAttribute("currentPage", "sign");
|
model.addAttribute("currentPage", "sign");
|
||||||
model.addAttribute("fonts", getFontNames());
|
model.addAttribute("fonts", getFontNames());
|
||||||
model.addAttribute("signatures", signatures);
|
|
||||||
return "sign";
|
return "sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,13 +31,6 @@ public class OtherWebController {
|
|||||||
return "misc/compress-pdf";
|
return "misc/compress-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/replace-and-invert-color-pdf")
|
|
||||||
@Hidden
|
|
||||||
public String replaceAndInvertColorPdfForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "replace-invert-color-pdf");
|
|
||||||
return "misc/replace-color";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/extract-image-scans")
|
@GetMapping("/extract-image-scans")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView extractImageScansForm() {
|
public ModelAndView extractImageScansForm() {
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.stereotype.Controller;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
|
||||||
import stirling.software.SPDF.service.SignatureService;
|
|
||||||
|
|
||||||
@Controller
|
|
||||||
@RequestMapping("/api/v1/general/")
|
|
||||||
public class SignatureController {
|
|
||||||
|
|
||||||
@Autowired private SignatureService signatureService;
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private UserServiceInterface userService;
|
|
||||||
|
|
||||||
@GetMapping("/sign/{fileName}")
|
|
||||||
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
|
|
||||||
throws IOException {
|
|
||||||
String username = "NON_SECURITY_USER";
|
|
||||||
if (userService != null) {
|
|
||||||
username = userService.getCurrentUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify access permission
|
|
||||||
if (!signatureService.hasAccessToFile(username, fileName)) {
|
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
|
|
||||||
return ResponseEntity.ok()
|
|
||||||
.contentType(MediaType.IMAGE_JPEG) // Adjust based on file type
|
|
||||||
.body(imageBytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,18 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.core.annotation.Order;
|
|
||||||
import org.springframework.core.io.ClassPathResource;
|
|
||||||
import org.springframework.core.io.FileSystemResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
@@ -35,7 +24,6 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|||||||
@ConfigurationProperties(prefix = "")
|
@ConfigurationProperties(prefix = "")
|
||||||
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
||||||
@Data
|
@Data
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
|
||||||
public class ApplicationProperties {
|
public class ApplicationProperties {
|
||||||
|
|
||||||
private Legal legal = new Legal();
|
private Legal legal = new Legal();
|
||||||
@@ -47,6 +35,7 @@ public class ApplicationProperties {
|
|||||||
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
|
||||||
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
|
||||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class AutoPipeline {
|
public static class AutoPipeline {
|
||||||
@@ -68,112 +57,16 @@ public class ApplicationProperties {
|
|||||||
private Boolean csrfDisabled;
|
private Boolean csrfDisabled;
|
||||||
private InitialLogin initialLogin = new InitialLogin();
|
private InitialLogin initialLogin = new InitialLogin();
|
||||||
private OAUTH2 oauth2 = new OAUTH2();
|
private OAUTH2 oauth2 = new OAUTH2();
|
||||||
private SAML2 saml2 = new SAML2();
|
|
||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
private String loginMethod = "all";
|
private String loginMethod = "all";
|
||||||
|
|
||||||
public Boolean isAltLogin() {
|
|
||||||
return saml2.getEnabled() || oauth2.getEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LoginMethods {
|
|
||||||
ALL("all"),
|
|
||||||
NORMAL("normal"),
|
|
||||||
OAUTH2("oauth2"),
|
|
||||||
SAML2("saml2");
|
|
||||||
|
|
||||||
private String method;
|
|
||||||
|
|
||||||
LoginMethods(String method) {
|
|
||||||
this.method = method;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isUserPass() {
|
|
||||||
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|
|
||||||
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOauth2Activ() {
|
|
||||||
return (oauth2 != null
|
|
||||||
&& oauth2.getEnabled()
|
|
||||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSaml2Activ() {
|
|
||||||
return (saml2 != null
|
|
||||||
&& saml2.getEnabled()
|
|
||||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class InitialLogin {
|
public static class InitialLogin {
|
||||||
private String username;
|
private String username;
|
||||||
@ToString.Exclude private String password;
|
@ToString.Exclude private String password;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
public static class SAML2 {
|
|
||||||
private Boolean enabled = false;
|
|
||||||
private Boolean autoCreateUser = false;
|
|
||||||
private Boolean blockRegistration = false;
|
|
||||||
private String registrationId = "stirling";
|
|
||||||
private String idpMetadataUri;
|
|
||||||
private String idpSingleLogoutUrl;
|
|
||||||
private String idpSingleLoginUrl;
|
|
||||||
private String idpIssuer;
|
|
||||||
private String idpCert;
|
|
||||||
private String privateKey;
|
|
||||||
private String spCert;
|
|
||||||
|
|
||||||
public InputStream getIdpMetadataUri() throws IOException {
|
|
||||||
if (idpMetadataUri.startsWith("classpath:")) {
|
|
||||||
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
|
|
||||||
.getInputStream();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
URI uri = new URI(idpMetadataUri);
|
|
||||||
URL url = uri.toURL();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("GET");
|
|
||||||
return connection.getInputStream();
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Resource getSpCert() {
|
|
||||||
if (spCert.startsWith("classpath:")) {
|
|
||||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(spCert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Resource getidpCert() {
|
|
||||||
if (idpCert.startsWith("classpath:")) {
|
|
||||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(idpCert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Resource getPrivateKey() {
|
|
||||||
if (privateKey.startsWith("classpath:")) {
|
|
||||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(privateKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class OAUTH2 {
|
public static class OAUTH2 {
|
||||||
private Boolean enabled = false;
|
private Boolean enabled = false;
|
||||||
@@ -243,7 +136,6 @@ public class ApplicationProperties {
|
|||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
private String tessdataDir;
|
private String tessdataDir;
|
||||||
private Boolean enableAlphaFunctionality;
|
private Boolean enableAlphaFunctionality;
|
||||||
private String enableAnalytics;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -283,14 +175,11 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class AutomaticallyGenerated {
|
public static class AutomaticallyGenerated {
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
private String UUID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class EnterpriseEdition {
|
public static class EnterpriseEdition {
|
||||||
private boolean enabled;
|
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
private int maxUsers;
|
|
||||||
private CustomMetadata customMetadata = new CustomMetadata();
|
private CustomMetadata customMetadata = new CustomMetadata();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class Provider implements ProviderInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isValid(Collection<String> value, String name) {
|
protected boolean isValid(Collection<String> value, String name) {
|
||||||
@@ -26,55 +27,66 @@ public class Provider implements ProviderInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<String> getScopes() {
|
public Collection<String> getScopes() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
|
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setScopes(String scopes) {
|
public void setScopes(String scopes) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
|
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUseAsUsername() {
|
public String getUseAsUsername() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
|
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUseAsUsername(String useAsUsername) {
|
public void setUseAsUsername(String useAsUsername) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
|
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getIssuer() {
|
public String getIssuer() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
|
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setIssuer(String issuer) {
|
public void setIssuer(String issuer) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
|
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientSecret() {
|
public String getClientSecret() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
|
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setClientSecret(String clientSecret) {
|
public void setClientSecret(String clientSecret) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
|
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
|
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setClientId(String clientId) {
|
public void setClientId(String clientId) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
|
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package stirling.software.SPDF.model;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class SignatureFile {
|
|
||||||
private String fileName;
|
|
||||||
private String category; // "Personal" or "Shared"
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user