Compare commits
1 Commits
Frooodle/a
...
Frooodle-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5f84f3fc9 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: Frooodle # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
custom: ['https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
12
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
12
.github/ISSUE_TEMPLATE/1-bug.yml
vendored
@@ -11,18 +11,6 @@ body:
|
|||||||
|
|
||||||
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
This issue form is for reporting bugs only. Please fill out the following sections to help us understand the issue you are facing.
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: installation-method
|
|
||||||
attributes:
|
|
||||||
label: Installation Method
|
|
||||||
description: |
|
|
||||||
Indicate whether you are using Docker or a local installation.
|
|
||||||
options:
|
|
||||||
- Docker
|
|
||||||
- Docker ultra lite
|
|
||||||
- Docker fat
|
|
||||||
- Local Installation
|
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: problem
|
id: problem
|
||||||
validations:
|
validations:
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
2
.github/ISSUE_TEMPLATE/2-feature.yml
vendored
@@ -1,8 +1,6 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Submit a new feature request.
|
description: Submit a new feature request.
|
||||||
title: "[Feature Request]: "
|
title: "[Feature Request]: "
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -9,8 +9,6 @@ updates:
|
|||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
open-pull-requests-limit: 10
|
|
||||||
rebase-strategy: "auto"
|
|
||||||
- package-ecosystem: "docker"
|
- package-ecosystem: "docker"
|
||||||
directory: "/" # Location of Dockerfile
|
directory: "/" # Location of Dockerfile
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
44
.github/labeler-config.yml
vendored
44
.github/labeler-config.yml
vendored
@@ -1,54 +1,20 @@
|
|||||||
Translation:
|
translation:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
- any-glob-to-any-file: 'src/main/resources/messages_*_*.properties'
|
||||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/templates/fragments/languages.html'
|
|
||||||
|
|
||||||
Front End:
|
Front End:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/resources/templates/**/*'
|
- any-glob-to-any-file: 'src/main/resources/templates/**'
|
||||||
- any-glob-to-any-file: 'src/main/resources/static/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
|
||||||
|
|
||||||
Java:
|
java:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
- any-glob-to-any-file: 'src/main/java/**/*.java'
|
||||||
|
|
||||||
Back End:
|
documentation:
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/settings.yml.template'
|
|
||||||
- any-glob-to-any-file: 'src/main/resources/banner.txt'
|
|
||||||
|
|
||||||
Security:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/provider/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/model/AuthenticationType.java'
|
|
||||||
|
|
||||||
API:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
|
|
||||||
|
|
||||||
Documentation:
|
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: '**/*.md'
|
- any-glob-to-any-file: '**/*.md'
|
||||||
- any-glob-to-any-file: 'scripts/counter_translation.py'
|
|
||||||
- any-glob-to-any-file: 'scripts/ignore_translation.toml'
|
|
||||||
|
|
||||||
Docker:
|
docker:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
- any-glob-to-any-file: 'Dockerfile'
|
- any-glob-to-any-file: 'Dockerfile'
|
||||||
- any-glob-to-any-file: 'Dockerfile-*'
|
- any-glob-to-any-file: 'Dockerfile-*'
|
||||||
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
|
||||||
|
|
||||||
Test:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: 'cucumber/**/*'
|
|
||||||
- any-glob-to-any-file: 'src/test**/*'
|
|
||||||
|
|
||||||
Github:
|
|
||||||
- changed-files:
|
|
||||||
- any-glob-to-any-file: '.github/**/*'
|
|
||||||
|
|||||||
2
.github/labels.yml
vendored
2
.github/labels.yml
vendored
@@ -89,5 +89,3 @@
|
|||||||
- name: "Test"
|
- name: "Test"
|
||||||
color: "FF9E1F"
|
color: "FF9E1F"
|
||||||
description: "Testing-related issues or pull requests"
|
description: "Testing-related issues or pull requests"
|
||||||
- name: "Stale"
|
|
||||||
color: "000000"
|
|
||||||
|
|||||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -10,3 +10,9 @@ Closes #(issue_number)
|
|||||||
- [ ] I have performed a self-review of my own code
|
- [ ] I have performed a self-review of my own code
|
||||||
- [ ] 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
|
||||||
|
|
||||||
|
## Contributor License Agreement
|
||||||
|
|
||||||
|
By submitting this pull request, I acknowledge and agree that my 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.
|
||||||
|
|
||||||
|
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)
|
||||||
|
|||||||
32
.github/release.yml
vendored
32
.github/release.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
changelog:
|
|
||||||
exclude:
|
|
||||||
labels:
|
|
||||||
- Documentation
|
|
||||||
- Test
|
|
||||||
- Github
|
|
||||||
|
|
||||||
categories:
|
|
||||||
- title: Bug Fixes
|
|
||||||
labels:
|
|
||||||
- Bug
|
|
||||||
|
|
||||||
- title: Enhancements
|
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
|
|
||||||
- title: Minor Enhancements
|
|
||||||
labels:
|
|
||||||
- Java
|
|
||||||
- Front End
|
|
||||||
|
|
||||||
- title: Docker Updates
|
|
||||||
labels:
|
|
||||||
- Docker
|
|
||||||
|
|
||||||
- title: Translation Changes
|
|
||||||
labels:
|
|
||||||
- Translation
|
|
||||||
|
|
||||||
- title: Other Changes
|
|
||||||
labels:
|
|
||||||
- "*"
|
|
||||||
254
.github/scripts/check_language_properties.py
vendored
254
.github/scripts/check_language_properties.py
vendored
@@ -1,254 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Ludy87
|
|
||||||
Description: This script processes .properties files for localization checks. It compares translation files in a branch with
|
|
||||||
a reference file to ensure consistency. The script performs two main checks:
|
|
||||||
1. Verifies that the number of lines (including comments and empty lines) in the translation files matches the reference file.
|
|
||||||
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
|
||||||
|
|
||||||
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
|
|
||||||
adjusting the format.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python script_name.py --reference-file <path_to_reference_file> --branch <branch_name> [--files <list_of_changed_files>]
|
|
||||||
"""
|
|
||||||
import copy
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def parse_properties_file(file_path):
|
|
||||||
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
|
|
||||||
properties_list = []
|
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
|
||||||
for line_number, line in enumerate(file, start=1):
|
|
||||||
stripped_line = line.strip()
|
|
||||||
|
|
||||||
# Empty lines
|
|
||||||
if not stripped_line:
|
|
||||||
properties_list.append(
|
|
||||||
{"line_number": line_number, "type": "empty", "content": ""}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Comments
|
|
||||||
if stripped_line.startswith("#"):
|
|
||||||
properties_list.append(
|
|
||||||
{
|
|
||||||
"line_number": line_number,
|
|
||||||
"type": "comment",
|
|
||||||
"content": stripped_line,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Key-value pairs
|
|
||||||
match = re.match(r"^([^=]+)=(.*)$", line)
|
|
||||||
if match:
|
|
||||||
key, value = match.groups()
|
|
||||||
properties_list.append(
|
|
||||||
{
|
|
||||||
"line_number": line_number,
|
|
||||||
"type": "entry",
|
|
||||||
"key": key.strip(),
|
|
||||||
"value": value.strip(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return properties_list
|
|
||||||
|
|
||||||
|
|
||||||
def write_json_file(file_path, updated_properties):
|
|
||||||
updated_lines = {entry["line_number"]: entry for entry in updated_properties}
|
|
||||||
|
|
||||||
# Sort by line numbers and retain comments and empty lines
|
|
||||||
all_lines = sorted(set(updated_lines.keys()))
|
|
||||||
|
|
||||||
original_format = []
|
|
||||||
for line in all_lines:
|
|
||||||
if line in updated_lines:
|
|
||||||
entry = updated_lines[line]
|
|
||||||
else:
|
|
||||||
entry = None
|
|
||||||
ref_entry = updated_lines[line]
|
|
||||||
if ref_entry["type"] in ["comment", "empty"]:
|
|
||||||
original_format.append(ref_entry)
|
|
||||||
elif entry is None:
|
|
||||||
# Add missing entries from the reference file
|
|
||||||
original_format.append(ref_entry)
|
|
||||||
elif entry["type"] == "entry":
|
|
||||||
# Replace entries with those from the current JSON
|
|
||||||
original_format.append(entry)
|
|
||||||
|
|
||||||
# Write back in the original format
|
|
||||||
with open(file_path, "w", encoding="utf-8") as file:
|
|
||||||
for entry in original_format:
|
|
||||||
if entry["type"] == "comment":
|
|
||||||
file.write(f"{entry['content']}\n")
|
|
||||||
elif entry["type"] == "empty":
|
|
||||||
file.write(f"{entry['content']}\n")
|
|
||||||
elif entry["type"] == "entry":
|
|
||||||
file.write(f"{entry['key']}={entry['value']}\n")
|
|
||||||
|
|
||||||
|
|
||||||
def update_missing_keys(reference_file, file_list, branch=""):
|
|
||||||
reference_properties = parse_properties_file(reference_file)
|
|
||||||
for file_path in file_list:
|
|
||||||
basename_current_file = os.path.basename(branch + file_path)
|
|
||||||
if (
|
|
||||||
basename_current_file == os.path.basename(reference_file)
|
|
||||||
or not file_path.endswith(".properties")
|
|
||||||
or not basename_current_file.startswith("messages_")
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
|
|
||||||
current_properties = parse_properties_file(branch + file_path)
|
|
||||||
updated_properties = []
|
|
||||||
for ref_entry in reference_properties:
|
|
||||||
ref_entry_copy = copy.deepcopy(ref_entry)
|
|
||||||
for current_entry in current_properties:
|
|
||||||
if current_entry["type"] == "entry":
|
|
||||||
if ref_entry_copy["type"] != "entry":
|
|
||||||
continue
|
|
||||||
if ref_entry_copy["key"] == current_entry["key"]:
|
|
||||||
ref_entry_copy["value"] = current_entry["value"]
|
|
||||||
updated_properties.append(ref_entry_copy)
|
|
||||||
write_json_file(branch + file_path, updated_properties)
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_missing_keys(reference_file, file_list, branch):
|
|
||||||
update_missing_keys(reference_file, file_list, branch + "/")
|
|
||||||
|
|
||||||
|
|
||||||
def read_properties(file_path):
|
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
|
||||||
return file.read().splitlines()
|
|
||||||
|
|
||||||
|
|
||||||
def check_for_differences(reference_file, file_list, branch):
|
|
||||||
reference_branch = reference_file.split("/")[0]
|
|
||||||
basename_reference_file = os.path.basename(reference_file)
|
|
||||||
|
|
||||||
report = []
|
|
||||||
report.append(
|
|
||||||
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
|
|
||||||
)
|
|
||||||
reference_lines = read_properties(reference_file)
|
|
||||||
has_differences = False
|
|
||||||
|
|
||||||
only_reference_file = True
|
|
||||||
|
|
||||||
for file_path in file_list:
|
|
||||||
basename_current_file = os.path.basename(branch + "/" + file_path)
|
|
||||||
if (
|
|
||||||
basename_current_file == basename_reference_file
|
|
||||||
or not file_path.endswith(".properties")
|
|
||||||
or not basename_current_file.startswith("messages_")
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
only_reference_file = False
|
|
||||||
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
|
|
||||||
current_lines = read_properties(branch + "/" + file_path)
|
|
||||||
reference_line_count = len(reference_lines)
|
|
||||||
current_line_count = len(current_lines)
|
|
||||||
|
|
||||||
if reference_line_count != current_line_count:
|
|
||||||
report.append("")
|
|
||||||
report.append("- **Test 1 Status:** ❌ Failed")
|
|
||||||
has_differences = True
|
|
||||||
if reference_line_count > current_line_count:
|
|
||||||
report.append(
|
|
||||||
f" - **Issue:** Missing lines! Comments, empty lines, or translation strings are missing. Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
|
||||||
)
|
|
||||||
elif reference_line_count < current_line_count:
|
|
||||||
report.append(
|
|
||||||
f" - **Issue:** Too many lines! Check your translation files! Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
|
||||||
)
|
|
||||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
|
||||||
else:
|
|
||||||
report.append("- **Test 1 Status:** ✅ Passed")
|
|
||||||
|
|
||||||
# Check for missing or extra keys
|
|
||||||
current_keys = []
|
|
||||||
reference_keys = []
|
|
||||||
for line in current_lines:
|
|
||||||
if not line.startswith("#") and line != "" and "=" in line:
|
|
||||||
key, _ = line.split("=", 1)
|
|
||||||
current_keys.append(key)
|
|
||||||
for line in reference_lines:
|
|
||||||
if not line.startswith("#") and line != "" and "=" in line:
|
|
||||||
key, _ = line.split("=", 1)
|
|
||||||
reference_keys.append(key)
|
|
||||||
|
|
||||||
current_keys_set = set(current_keys)
|
|
||||||
reference_keys_set = set(reference_keys)
|
|
||||||
missing_keys = current_keys_set.difference(reference_keys_set)
|
|
||||||
extra_keys = reference_keys_set.difference(current_keys_set)
|
|
||||||
missing_keys_list = list(missing_keys)
|
|
||||||
extra_keys_list = list(extra_keys)
|
|
||||||
|
|
||||||
if missing_keys_list or extra_keys_list:
|
|
||||||
has_differences = True
|
|
||||||
missing_keys_str = "`, `".join(missing_keys_list)
|
|
||||||
extra_keys_str = "`, `".join(extra_keys_list)
|
|
||||||
report.append("- **Test 2 Status:** ❌ Failed")
|
|
||||||
if missing_keys_list:
|
|
||||||
report.append(
|
|
||||||
f" - **Issue:** There are keys in ***{basename_current_file}*** `{missing_keys_str}` that are not present in ***{basename_reference_file}***!"
|
|
||||||
)
|
|
||||||
if extra_keys_list:
|
|
||||||
report.append(
|
|
||||||
f" - **Issue:** There are keys in ***{basename_reference_file}*** `{extra_keys_str}` that are not present in ***{basename_current_file}***!"
|
|
||||||
)
|
|
||||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
|
||||||
else:
|
|
||||||
report.append("- **Test 2 Status:** ✅ Passed")
|
|
||||||
# if has_differences:
|
|
||||||
# report.append("")
|
|
||||||
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
|
|
||||||
report.append("")
|
|
||||||
report.append("---")
|
|
||||||
report.append("")
|
|
||||||
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
|
|
||||||
# update_missing_keys(reference_file, update_file_list)
|
|
||||||
# report.append("---")
|
|
||||||
# report.append("")
|
|
||||||
if has_differences:
|
|
||||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
|
||||||
else:
|
|
||||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
|
||||||
|
|
||||||
if not only_reference_file:
|
|
||||||
print("\n".join(report))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
|
||||||
parser.add_argument(
|
|
||||||
"--reference-file",
|
|
||||||
required=True,
|
|
||||||
help="Path to the reference file.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--branch",
|
|
||||||
type=str,
|
|
||||||
required=True,
|
|
||||||
help="Branch name.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--files",
|
|
||||||
nargs="+",
|
|
||||||
required=False,
|
|
||||||
help="List of changed files, separated by spaces.",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
file_list = args.files
|
|
||||||
if file_list is None:
|
|
||||||
file_list = glob.glob(
|
|
||||||
os.getcwd() + "/src/**/messages_*.properties", recursive=True
|
|
||||||
)
|
|
||||||
update_missing_keys(args.reference_file, file_list)
|
|
||||||
else:
|
|
||||||
check_for_differences(args.reference_file, file_list, args.branch)
|
|
||||||
1
.github/scripts/check_tabulator.py
vendored
1
.github/scripts/check_tabulator.py
vendored
@@ -1,5 +1,4 @@
|
|||||||
"""check_tabulator.py"""
|
"""check_tabulator.py"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
60
.github/workflows/build.yml
vendored
60
.github/workflows/build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Build repo
|
name: "Build repo"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,72 +17,20 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
|
||||||
jdk-version: [17, 21]
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK ${{ matrix.jdk-version }}
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: ${{ matrix.jdk-version }}
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- name: Set up Gradle
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
uses: gradle/actions/setup-gradle@v4
|
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: ./gradlew build --no-build-cache
|
run: ./gradlew build --no-build-cache
|
||||||
|
|
||||||
docker-compose-tests:
|
|
||||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
|
||||||
# (github.event_name == 'pull_request' &&
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'licenses') == false &&
|
|
||||||
# (
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Front End') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Java') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Back End') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Security') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'API') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Docker') ||
|
|
||||||
# contains(github.event.pull_request.labels.*.name, 'Test')
|
|
||||||
# )
|
|
||||||
# )
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout Repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Java 17
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
java-version: "17"
|
|
||||||
distribution: "adopt"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Install Docker Compose
|
|
||||||
run: |
|
|
||||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
|
||||||
sudo chmod +x /usr/local/bin/docker-compose
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.7"
|
|
||||||
|
|
||||||
- name: Pip requirements
|
|
||||||
run: |
|
|
||||||
pip install -r ./cucumber/requirements.txt
|
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
|
||||||
run: |
|
|
||||||
chmod +x ./test.sh
|
|
||||||
./test.sh
|
|
||||||
|
|||||||
202
.github/workflows/check_properties.yml
vendored
202
.github/workflows/check_properties.yml
vendored
@@ -1,202 +0,0 @@
|
|||||||
name: Check Properties Files
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
paths:
|
|
||||||
- "src/main/resources/messages_*.properties"
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- "src/main/resources/messages_en_GB.properties"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-files:
|
|
||||||
if: github.event_name == 'pull_request_target'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout PR branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
|
||||||
path: pr-branch
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Checkout main branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
path: main-branch
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
|
|
||||||
- name: Install GitHub CLI
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y gh
|
|
||||||
|
|
||||||
- name: Fetch PR changed files
|
|
||||||
id: fetch-pr-changes
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "Fetching PR changed files..."
|
|
||||||
cd pr-branch
|
|
||||||
gh repo set-default ${{ github.repository }}
|
|
||||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
|
|
||||||
cd ..
|
|
||||||
echo $(cat changed_files.txt)
|
|
||||||
BRANCH_PATH="pr-branch"
|
|
||||||
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
|
|
||||||
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
|
|
||||||
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
|
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
|
||||||
echo "Branch: ${BRANCH_PATH}"
|
|
||||||
|
|
||||||
- name: Determine reference file
|
|
||||||
id: determine-file
|
|
||||||
run: |
|
|
||||||
echo "Determining reference file..."
|
|
||||||
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
|
|
||||||
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
|
|
||||||
|
|
||||||
- name: Show REFERENCE_FILE
|
|
||||||
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
|
|
||||||
|
|
||||||
- name: Run Python script to check files
|
|
||||||
id: run-check
|
|
||||||
run: |
|
|
||||||
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
|
|
||||||
|
|
||||||
- name: Capture output
|
|
||||||
id: capture-output
|
|
||||||
run: |
|
|
||||||
if [ -f failure.txt ] && [ -s failure.txt ]; then
|
|
||||||
echo "Test failed, capturing output..."
|
|
||||||
ERROR_OUTPUT=$(cat failure.txt)
|
|
||||||
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
|
|
||||||
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
|
|
||||||
echo "EOF" >> $GITHUB_ENV
|
|
||||||
echo $ERROR_OUTPUT
|
|
||||||
else
|
|
||||||
echo "No errors found."
|
|
||||||
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Post comment on PR
|
|
||||||
if: env.ERROR_OUTPUT != ''
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
|
|
||||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
|
||||||
const prNumber = context.issue.number;
|
|
||||||
|
|
||||||
// Find existing comment
|
|
||||||
const comments = await github.rest.issues.listComments({
|
|
||||||
owner: repoOwner,
|
|
||||||
repo: repoName,
|
|
||||||
issue_number: prNumber
|
|
||||||
});
|
|
||||||
|
|
||||||
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
|
||||||
|
|
||||||
// Only allow the action user to update comments
|
|
||||||
const expectedActor = "github-actions[bot]";
|
|
||||||
|
|
||||||
if (comment && comment.user.login === expectedActor) {
|
|
||||||
// Update existing comment
|
|
||||||
await github.rest.issues.updateComment({
|
|
||||||
owner: repoOwner,
|
|
||||||
repo: repoName,
|
|
||||||
comment_id: comment.id,
|
|
||||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
|
||||||
});
|
|
||||||
console.log("Updated existing comment.");
|
|
||||||
} else if (!comment) {
|
|
||||||
// Create new comment if no existing comment is found
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner: repoOwner,
|
|
||||||
repo: repoName,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
|
||||||
});
|
|
||||||
console.log("Created new comment.");
|
|
||||||
} else {
|
|
||||||
console.log("Comment update attempt denied. Actor does not match.");
|
|
||||||
}
|
|
||||||
|
|
||||||
# - 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: Add translation keys
|
|
||||||
# run: |
|
|
||||||
# cd ${{ env.BRANCH_PATH }}
|
|
||||||
# git add src/main/resources/messages_*.properties
|
|
||||||
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
|
||||||
# git commit -m "Update translation files" || echo "No changes to commit"
|
|
||||||
# - name: Push
|
|
||||||
# if: env.CHANGES_DETECTED == 'true'
|
|
||||||
# run: |
|
|
||||||
# cd pr-branch
|
|
||||||
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
|
|
||||||
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
|
|
||||||
|
|
||||||
update-translations-main:
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: "3.x"
|
|
||||||
|
|
||||||
- name: Run Python script to check files
|
|
||||||
id: run-check
|
|
||||||
run: |
|
|
||||||
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
|
|
||||||
|
|
||||||
- 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: Add translation keys
|
|
||||||
run: |
|
|
||||||
git add src/main/resources/messages_*.properties
|
|
||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Create Pull Request
|
|
||||||
id: cpr
|
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
|
||||||
uses: peter-evans/create-pull-request@v6
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
commit-message: "Update translation files"
|
|
||||||
committer: GitHub Action <action@github.com>
|
|
||||||
author: GitHub Action <action@github.com>
|
|
||||||
signoff: true
|
|
||||||
branch: update_translation_files
|
|
||||||
title: "Update translation files"
|
|
||||||
body: |
|
|
||||||
Auto-generated by [create-pull-request][1]
|
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
|
||||||
labels: Translation
|
|
||||||
draft: false
|
|
||||||
delete-branch: true
|
|
||||||
@@ -11,9 +11,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/labeler@v5
|
||||||
- name: Apply Labels
|
|
||||||
uses: actions/labeler@v5
|
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
configuration-path: .github/labeler-config.yml
|
configuration-path: .github/labeler-config.yml
|
||||||
23
.github/workflows/licenses-update.yml
vendored
23
.github/workflows/licenses-update.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean generateLicenseReport
|
run: ./gradlew clean generateLicenseReport
|
||||||
@@ -36,8 +36,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up git config
|
- name: Set up git config
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
|
|
||||||
- name: Run git add
|
- name: Run git add
|
||||||
run: |
|
run: |
|
||||||
@@ -45,7 +45,6 @@ jobs:
|
|||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
with:
|
with:
|
||||||
@@ -58,22 +57,6 @@ jobs:
|
|||||||
title: "Update 3rd Party Licenses"
|
title: "Update 3rd Party Licenses"
|
||||||
body: |
|
body: |
|
||||||
Auto-generated by [create-pull-request][1]
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
labels: licenses
|
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
|
||||||
- name: Auto approve
|
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
|
||||||
run: gh pr review --approve "${{ steps.cpr.outputs.pull-request-number }}"
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Enable auto-merge
|
|
||||||
if: steps.cpr.outputs.pull-request-operation == 'created'
|
|
||||||
uses: peter-evans/enable-pull-request-automerge@v3
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}
|
|
||||||
merge-method: squash # Choose the merge method: merge, squash, or rebase
|
|
||||||
|
|||||||
9
.github/workflows/push-docker.yml
vendored
9
.github/workflows/push-docker.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ jobs:
|
|||||||
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -111,6 +111,7 @@ jobs:
|
|||||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||||
platforms: linux/amd64,linux/arm64/v8
|
platforms: linux/amd64,linux/arm64/v8
|
||||||
|
|
||||||
|
|
||||||
- name: Generate tags fat
|
- name: Generate tags fat
|
||||||
id: meta3
|
id: meta3
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -124,7 +125,7 @@ jobs:
|
|||||||
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
|||||||
2
.github/workflows/releaseArtifacts.yml
vendored
2
.github/workflows/releaseArtifacts.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.7
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@v4
|
- uses: gradle/actions/setup-gradle@v3
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
|||||||
10
.github/workflows/sync_files.yml
vendored
10
.github/workflows/sync_files.yml
vendored
@@ -28,8 +28,8 @@ jobs:
|
|||||||
run: python .github/scripts/gradle_to_chart.py
|
run: python .github/scripts/gradle_to_chart.py
|
||||||
- name: Set up git config
|
- name: Set up git config
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
- name: Run git add
|
- name: Run git add
|
||||||
run: |
|
run: |
|
||||||
git add .
|
git add .
|
||||||
@@ -51,7 +51,6 @@ jobs:
|
|||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
labels: github-actions
|
|
||||||
sync-readme:
|
sync-readme:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -66,8 +65,8 @@ jobs:
|
|||||||
run: python scripts/counter_translation.py
|
run: python scripts/counter_translation.py
|
||||||
- name: Set up git config
|
- name: Set up git config
|
||||||
run: |
|
run: |
|
||||||
git config --global user.name "github-actions[bot]"
|
git config --global user.email "GitHub Action <action@github.com>"
|
||||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
git config --global user.name "GitHub Action <action@github.com>"
|
||||||
- name: Run git add
|
- name: Run git add
|
||||||
run: |
|
run: |
|
||||||
git add .
|
git add .
|
||||||
@@ -89,4 +88,3 @@ jobs:
|
|||||||
[1]: https://github.com/peter-evans/create-pull-request
|
[1]: https://github.com/peter-evans/create-pull-request
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
labels: Documentation,Translation,github-actions
|
|
||||||
|
|||||||
47
.github/workflows/test.yml
vendored
Normal file
47
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Docker Compose Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "**.gradle"
|
||||||
|
- "!src/main/java/resources/messages*"
|
||||||
|
- "exampleYmlFiles/**"
|
||||||
|
- "Dockerfile"
|
||||||
|
- "Dockerfile**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Java 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "adopt"
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Install Docker Compose
|
||||||
|
run: |
|
||||||
|
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
|
# sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: "3.7"
|
||||||
|
|
||||||
|
- name: Pip requirements
|
||||||
|
run: |
|
||||||
|
pip install -r ./cucumber/requirements.txt
|
||||||
|
|
||||||
|
- name: Run Docker Compose Tests
|
||||||
|
run: |
|
||||||
|
chmod +x ./test.sh
|
||||||
|
./test.sh
|
||||||
45
.gitignore
vendored
45
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
### Eclipse ###
|
### Eclipse ###
|
||||||
.metadata
|
.metadata
|
||||||
bin/
|
bin/
|
||||||
@@ -20,6 +22,7 @@ customFiles/
|
|||||||
configs/
|
configs/
|
||||||
watchedFolders/
|
watchedFolders/
|
||||||
|
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
.gradle
|
.gradle
|
||||||
.lock
|
.lock
|
||||||
@@ -116,48 +119,12 @@ watchedFolders/
|
|||||||
*.db
|
*.db
|
||||||
/build
|
/build
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
/.vscode
|
||||||
__pycache__/
|
/.idea
|
||||||
*.py[cod]
|
|
||||||
*.pyo
|
|
||||||
|
|
||||||
# Virtual environments
|
|
||||||
.env*
|
|
||||||
.venv*
|
|
||||||
env*/
|
|
||||||
venv*/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# VS Code
|
|
||||||
/.vscode/**/*
|
|
||||||
!/.vscode/settings.json
|
|
||||||
|
|
||||||
# IntelliJ IDEA
|
|
||||||
.idea/
|
|
||||||
*.iml
|
|
||||||
out/
|
|
||||||
|
|
||||||
# Ignore Mac DS_Store files
|
# Ignore Mac DS_Store files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
# cucumber
|
#cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
# Certs
|
|
||||||
*.p12
|
|
||||||
*.pem
|
|
||||||
*.crt
|
|
||||||
*.cer
|
|
||||||
*.der
|
|
||||||
*.key
|
|
||||||
*.csr
|
|
||||||
|
|
||||||
# cache
|
|
||||||
.ruff_cache
|
|
||||||
.mypy_cache
|
|
||||||
.pytest_cache
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
|
|||||||
53
.vscode/settings.json
vendored
53
.vscode/settings.json
vendored
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"java.compile.nullAnalysis.mode": "automatic",
|
|
||||||
"files.eol": "auto",
|
|
||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
|
||||||
"black-formatter.args": ["--line-length", "127"],
|
|
||||||
"flake8.args": ["--max-line-length", "127"],
|
|
||||||
"pylint.args": ["max-line-length", "127"],
|
|
||||||
"[java]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[python]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[gradle-build]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[gradle]": {
|
|
||||||
"editor.tabSize": 4,
|
|
||||||
"editor.detectIndentation": false,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[html]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.rulers": [127],
|
|
||||||
"files.trimFinalNewlines": false,
|
|
||||||
"files.insertFinalNewline": false
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.rulers": [127]
|
|
||||||
},
|
|
||||||
"[yaml]": {
|
|
||||||
"files.trimFinalNewlines": false,
|
|
||||||
"files.insertFinalNewline": false
|
|
||||||
},
|
|
||||||
"diffEditor.maxComputationTime": 0,
|
|
||||||
"editor.wordSegmenterLocales": null,
|
|
||||||
"editor.guides.bracketPairs": "active",
|
|
||||||
"editor.guides.bracketPairsHorizontal": "active",
|
|
||||||
"files.insertFinalNewline": true,
|
|
||||||
"files.trimFinalNewlines": true,
|
|
||||||
"files.trimTrailingWhitespace": true,
|
|
||||||
"editor.indentSize": "tabSize",
|
|
||||||
"editor.stickyScroll.enabled": false,
|
|
||||||
"editor.minimap.enabled": false,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
}
|
|
||||||
10
Dockerfile
10
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.2
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -39,16 +39,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
libreoffice \
|
libreoffice \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
ocrmypdf \
|
ocrmypdf \
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 \
|
python3 && \
|
||||||
py3-pip && \
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ RUN DOCKER_ENABLE_SECURITY=true \
|
|||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.20.2
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
# Copy necessary files
|
# Copy necessary files
|
||||||
COPY scripts /scripts
|
COPY scripts /scripts
|
||||||
@@ -61,10 +61,10 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
# python3/pip
|
# python3/pip
|
||||||
python3 \
|
python3 && \
|
||||||
py3-pip && \
|
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
|
||||||
# uno unoconv and HTML
|
# uno unoconv and HTML
|
||||||
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
|
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
|
||||||
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
mv /usr/share/tessdata /usr/share/tessdata-original && \
|
||||||
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
|
||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.20.2
|
FROM alpine:3.20.0
|
||||||
|
|
||||||
ARG VERSION_TAG
|
ARG VERSION_TAG
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | |
|
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
|
||||||
|
|||||||
687
LICENSE
687
LICENSE
@@ -1,21 +1,674 @@
|
|||||||
MIT License
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (c) 2024 Stirling Tools
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Preamble
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
The GNU General Public License is a free, copyleft license for
|
||||||
copies or substantial portions of the Software.
|
software and other kinds of works.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
The licenses for most software and other practical works are designed
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
to take away your freedom to share and change the works. By contrast,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
share and change all versions of a program--to make sure it remains free
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
GNU General Public License for most of our software; it applies also to
|
||||||
SOFTWARE.
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -5,6 +5,8 @@
|
|||||||
[](https://discord.gg/Cn8pWhQRxZ)
|
[](https://discord.gg/Cn8pWhQRxZ)
|
||||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
|
[](https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL)
|
||||||
|
[](https://github.com/sponsors/Frooodle)
|
||||||
|
|
||||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||||
[<img src="https://www.ssdnodes.com/wp-content/uploads/2023/11/footer-logo.svg" alt="Name" height="40">](https://www.ssdnodes.com/manage/aff.php?aff=2216®ister=true)
|
[<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)
|
||||||
@@ -22,7 +24,6 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- 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
|
|
||||||
- API for integration with external scripts
|
- API for integration with external scripts
|
||||||
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
|
||||||
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
- Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
|
||||||
@@ -45,7 +46,6 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- 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 ontop of each other
|
|
||||||
|
|
||||||
### **Conversion Operations**
|
### **Conversion Operations**
|
||||||
|
|
||||||
@@ -82,7 +82,6 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
|||||||
- 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
|
|
||||||
|
|
||||||
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
|
||||||
|
|
||||||
@@ -166,46 +165,43 @@ Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR
|
|||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling PDF currently supports 38!
|
Stirling PDF currently supports 33!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| ------------------------------------------- | -------------------------------------- |
|
| ------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
|
||||||
| Catalan (Català) (ca_CA) |  |
|
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
|
||||||
| Danish (Dansk) (da_DK) |  |
|
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
|
||||||
| Italian (Italiano) (it_IT) |  |
|
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
|
||||||
| Korean (한국어) (ko_KR) |  |
|
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
|
||||||
| Polish (Polski) (pl_PL) |  |
|
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
|
||||||
| Romanian (Română) (ro_RO) |  |
|
|
||||||
| Russian (Русский) (ru_RU) |  |
|
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
|
||||||
| Spanish (Español) (es_ES) |  |
|
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
|
||||||
| Thai (ไทย) (th_TH) |  |
|
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
|
| Polish (Polski) (pl_PL) |  |
|
||||||
|
| Romanian (Română) (ro_RO) |  |
|
||||||
|
| Korean (한국어) (ko_KR) |  |
|
||||||
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
|
| Russian (Русский) (ru_RU) |  |
|
||||||
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
|
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||||
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
|
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
|
| Thai (ไทย) (th_TH) |  |
|
||||||
|
|
||||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||||
|
|
||||||
@@ -238,7 +234,7 @@ The Current list of settings is
|
|||||||
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
|
||||||
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:
|
||||||
@@ -267,7 +263,6 @@ security:
|
|||||||
clientId: '' # Client ID from your provider
|
clientId: '' # Client ID from your provider
|
||||||
clientSecret: '' # Client Secret from your provider
|
clientSecret: '' # Client Secret from your provider
|
||||||
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
|
||||||
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
|
|
||||||
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
useAsUsername: email # Default is 'email'; custom fields can be used as the username
|
||||||
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
|
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
|
||||||
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||||
|
|||||||
124
build.gradle
124
build.gradle
@@ -1,43 +1,29 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.3.3"
|
id "org.springframework.boot" version "3.3.0"
|
||||||
id "io.spring.dependency-management" version "1.1.6"
|
id "io.spring.dependency-management" version "1.1.5"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
id "edu.sc.seis.launch4j" version "3.0.6"
|
id "edu.sc.seis.launch4j" version "3.0.5"
|
||||||
id "com.diffplug.spotless" version "6.25.0"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.8"
|
||||||
//id "nebula.lint" version "19.0.3"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.3.3"
|
springBootVersion = "3.3.2"
|
||||||
pdfboxVersion = "3.0.3"
|
|
||||||
logbackVersion = "1.5.7"
|
|
||||||
imageioVersion = "3.11.0"
|
|
||||||
lombokVersion = "1.18.34"
|
|
||||||
bouncycastleVersion = "1.78.1"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.28.3"
|
version = "0.27.0"
|
||||||
|
|
||||||
java {
|
// 17 is lowest but we support and recommend 21
|
||||||
// 17 is lowest but we support and recommend 21
|
sourceCompatibility = "17"
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
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 {
|
||||||
@@ -54,10 +40,8 @@ sourceSets {
|
|||||||
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||||
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||||
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
|
||||||
exclude "stirling/software/SPDF/model/Authority.java"
|
exclude "stirling/software/SPDF/model/Authority.java"
|
||||||
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
||||||
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
|
||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
exclude "stirling/software/SPDF/repository/**"
|
exclude "stirling/software/SPDF/repository/**"
|
||||||
}
|
}
|
||||||
@@ -107,48 +91,39 @@ spotless {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//gradleLint {
|
|
||||||
// rules=['unused-dependency']
|
|
||||||
// }
|
|
||||||
tasks.wrapper {
|
tasks.wrapper {
|
||||||
gradleVersion = "8.7"
|
gradleVersion = "8.7"
|
||||||
}
|
}
|
||||||
//tasks.withType(JavaCompile) {
|
|
||||||
// options.compilerArgs << "-Xlint:deprecation"
|
|
||||||
//}
|
|
||||||
configurations.all {
|
|
||||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
|
||||||
}
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//security updates
|
//security updates
|
||||||
|
implementation "ch.qos.logback:logback-classic:1.5.6"
|
||||||
|
implementation "ch.qos.logback:logback-core:1.5.6"
|
||||||
implementation "org.springframework:spring-webmvc:6.1.9"
|
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.1.3")
|
||||||
|
|
||||||
// implementation "org.yaml:snakeyaml:2.2"
|
// implementation "org.yaml:snakeyaml:2.2"
|
||||||
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
|
||||||
|
|
||||||
// Exclude Tomcat and include Jetty
|
// Exclude Tomcat and include Jetty
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
|
implementation("org.springframework.boot:spring-boot-starter-web:$springBootVersion") {
|
||||||
|
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||||
|
}
|
||||||
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"
|
||||||
|
|
||||||
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"
|
||||||
runtimeOnly "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
runtimeOnly "com.h2database:h2:2.1.214"
|
implementation "com.h2database:h2:2.1.214"
|
||||||
// implementation "com.h2database:h2:2.2.224"
|
// implementation "com.h2database:h2:2.2.224"
|
||||||
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3'
|
}
|
||||||
}
|
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
|
||||||
@@ -156,25 +131,26 @@ if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
|||||||
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
implementation "org.apache.xmlgraphics:batik-all:1.17"
|
||||||
|
|
||||||
// TwelveMonkeys
|
// TwelveMonkeys
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
|
implementation "com.twelvemonkeys.imageio:imageio-batik:3.10.1"
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-bmp:$imageioVersion"
|
implementation "com.twelvemonkeys.imageio:imageio-bmp:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-hdr:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-hdr:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-icns:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-icns:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-iff:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-iff:3.10.1"
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-jpeg:$imageioVersion"
|
implementation "com.twelvemonkeys.imageio:imageio-jpeg:3.11.0"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@
|
// implementation "com.twelvemonkeys.imageio:imageio-pcx:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-pict:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-pnm:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-psd:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-sgi:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-tga:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-thumbsdb:3.10.1"
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-tiff:$imageioVersion"
|
implementation "com.twelvemonkeys.imageio:imageio-tiff:3.10.1"
|
||||||
runtimeOnly "com.twelvemonkeys.imageio:imageio-webp:$imageioVersion"
|
implementation "com.twelvemonkeys.imageio:imageio-webp:3.10.1"
|
||||||
// runtimeOnly "com.twelvemonkeys.imageio:imageio-xwd:$imageioVersion"
|
// implementation "com.twelvemonkeys.imageio:imageio-xwd:3.10.1"
|
||||||
|
|
||||||
implementation "commons-io:commons-io:2.16.1"
|
implementation "commons-io:commons-io:2.16.1"
|
||||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||||
|
|
||||||
//general PDF
|
//general PDF
|
||||||
|
|
||||||
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
// https://mvnrepository.com/artifact/com.opencsv/opencsv
|
||||||
@@ -182,33 +158,33 @@ if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
|||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
|
implementation ("org.apache.pdfbox:pdfbox:3.0.2") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
|
||||||
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
|
implementation ("org.apache.pdfbox:xmpbox:3.0.2") {
|
||||||
exclude group: "commons-logging", module: "commons-logging"
|
exclude group: "commons-logging", module: "commons-logging"
|
||||||
}
|
}
|
||||||
|
implementation "com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4"
|
||||||
|
|
||||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
implementation "org.bouncycastle:bcprov-jdk18on:1.78.1"
|
||||||
|
implementation "org.bouncycastle:bcpkix-jdk18on:1.78.1"
|
||||||
implementation "org.bouncycastle:bcprov-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.3"
|
implementation "io.micrometer:micrometer-core:1.13.0"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.22.0"
|
implementation "org.commonmark:commonmark:0.22.0"
|
||||||
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
implementation "org.commonmark:commonmark-ext-gfm-tables:0.22.0"
|
||||||
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
|
||||||
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
|
implementation "com.bucket4j:bucket4j_jdk17-core:8.12.1"
|
||||||
implementation "com.fathzer:javaluator:3.0.4"
|
|
||||||
|
implementation "com.fathzer:javaluator:3.0.4"
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
developmentOnly("org.springframework.boot:spring-boot-devtools:$springBootVersion")
|
||||||
compileOnly "org.projectlombok:lombok:$lombokVersion"
|
compileOnly "org.projectlombok:lombok:1.18.32"
|
||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:1.18.32"
|
||||||
|
|
||||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
testImplementation 'org.mockito:mockito-inline:3.12.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
tasks.withType(JavaCompile).configureEach {
|
||||||
@@ -219,7 +195,7 @@ compileJava {
|
|||||||
options.compilerArgs << "-parameters"
|
options.compilerArgs << "-parameters"
|
||||||
}
|
}
|
||||||
|
|
||||||
task writeVersion {
|
task writeVersion {
|
||||||
def propsFile = file("src/main/resources/version.properties")
|
def propsFile = file("src/main/resources/version.properties")
|
||||||
def props = new Properties()
|
def props = new Properties()
|
||||||
props.setProperty("version", version)
|
props.setProperty("version", version)
|
||||||
@@ -249,6 +225,6 @@ tasks.named("test") {
|
|||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
task printVersion {
|
task printVersion {
|
||||||
println project.version
|
println project.version
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: 0.28.3
|
appVersion: 0.26.2
|
||||||
description: locally hosted web application that allows you to perform various operations
|
description: locally hosted web application that allows you to perform various operations
|
||||||
on PDF files
|
on PDF files
|
||||||
home: https://github.com/Stirling-Tools/Stirling-PDF
|
home: https://github.com/Stirling-Tools/Stirling-PDF
|
||||||
|
|||||||
@@ -62,10 +62,8 @@ spec:
|
|||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
securityContext:
|
securityContext:
|
||||||
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
|
||||||
env:
|
|
||||||
- name: SYSTEM_ROOTURIPATH
|
|
||||||
value: {{ .Values.rootPath}}
|
|
||||||
{{- if .Values.envs }}
|
{{- if .Values.envs }}
|
||||||
|
env:
|
||||||
{{ toYaml .Values.envs | indent 8 }}
|
{{ toYaml .Values.envs | indent 8 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.extraArgs }}
|
{{- if .Values.extraArgs }}
|
||||||
@@ -77,13 +75,13 @@ spec:
|
|||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: {{ .Values.rootPath}}
|
path: /
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.livenessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.liveness | indent 10 }}
|
{{ toYaml .Values.probes.liveness | indent 10 }}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: {{ .Values.rootPath}}
|
path: /
|
||||||
port: http
|
port: http
|
||||||
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
{{ toYaml .Values.probes.readinessHttpGetConfig | indent 12 }}
|
||||||
{{ toYaml .Values.probes.readiness | indent 10 }}
|
{{ toYaml .Values.probes.readiness | indent 10 }}
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ secret:
|
|||||||
commonLabels: {}
|
commonLabels: {}
|
||||||
# team_name: dev
|
# team_name: dev
|
||||||
|
|
||||||
# rootpath for the application
|
|
||||||
rootPath: /
|
|
||||||
|
|
||||||
envs: []
|
envs: []
|
||||||
# - name: UI_APP_NAME
|
# - name: UI_APP_NAME
|
||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
@@ -27,6 +24,8 @@ envs: []
|
|||||||
# value: "Stirling PDF"
|
# value: "Stirling PDF"
|
||||||
# - name: ALLOW_GOOGLE_VISIBILITY
|
# - name: ALLOW_GOOGLE_VISIBILITY
|
||||||
# value: "true"
|
# value: "true"
|
||||||
|
# - name: APP_ROOT_PATH
|
||||||
|
# value: "/"
|
||||||
# - name: APP_LOCALE
|
# - name: APP_LOCALE
|
||||||
# value: "en_GB"
|
# value: "en_GB"
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -92,10 +92,10 @@ Feature: API Validation
|
|||||||
| threshold | 90 |
|
| threshold | 90 |
|
||||||
| whitePercent | 99.9 |
|
| whitePercent | 99.9 |
|
||||||
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
When I send the API request to the endpoint "/api/v1/misc/remove-blanks"
|
||||||
Then the response content type should be "application/octet-stream"
|
Then the response content type should be "application/pdf"
|
||||||
And the response file should have extension ".zip"
|
|
||||||
And the response ZIP should contain 1 files
|
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
|
And the response PDF should contain 0 pages
|
||||||
|
And the response status code should be 200
|
||||||
|
|
||||||
@positive @flatten
|
@positive @flatten
|
||||||
Scenario: Flatten PDF
|
Scenario: Flatten PDF
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Feature: API Validation
|
|||||||
|
|
||||||
|
|
||||||
@extract-images
|
@extract-images
|
||||||
Scenario Outline: Extract Image Scans duplicates
|
Scenario Outline: Extract Image Scans
|
||||||
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/images.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
| parameter | value |
|
| parameter | value |
|
||||||
@@ -103,7 +103,7 @@ Feature: API Validation
|
|||||||
When I send the API request to the endpoint "/api/v1/misc/extract-images"
|
When I send the API request to the endpoint "/api/v1/misc/extract-images"
|
||||||
Then the response content type should be "application/octet-stream"
|
Then the response content type should be "application/octet-stream"
|
||||||
And the response file should have extension ".zip"
|
And the response file should have extension ".zip"
|
||||||
And the response ZIP should contain 2 files
|
And the response ZIP should contain 20 files
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
And the response status code should be 200
|
And the response status code should be 200
|
||||||
|
|
||||||
@@ -112,3 +112,5 @@ Feature: API Validation
|
|||||||
| png |
|
| png |
|
||||||
| gif |
|
| gif |
|
||||||
| jpeg |
|
| jpeg |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ ignore = [
|
|||||||
'text',
|
'text',
|
||||||
]
|
]
|
||||||
|
|
||||||
[da_DK]
|
|
||||||
ignore = [
|
|
||||||
'language.direction',
|
|
||||||
]
|
|
||||||
|
|
||||||
[de_DE]
|
[de_DE]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
@@ -92,11 +87,6 @@ ignore = [
|
|||||||
'watermark.type.2',
|
'watermark.type.2',
|
||||||
]
|
]
|
||||||
|
|
||||||
[ga_IE]
|
|
||||||
ignore = [
|
|
||||||
'language.direction',
|
|
||||||
]
|
|
||||||
|
|
||||||
[hi_IN]
|
[hi_IN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@@ -181,9 +171,7 @@ ignore = [
|
|||||||
|
|
||||||
[pt_BR]
|
[pt_BR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'changeMetadata.trapped',
|
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'pipelineOptions.pipelineHeader',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[pt_PT]
|
[pt_PT]
|
||||||
@@ -242,14 +230,6 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
[vi_VN]
|
|
||||||
ignore = [
|
|
||||||
'language.direction',
|
|
||||||
'pipeline.title',
|
|
||||||
'pipelineOptions.pipelineHeader',
|
|
||||||
'showJS.tags',
|
|
||||||
]
|
|
||||||
|
|
||||||
[zh_CN]
|
[zh_CN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Ludy87
|
|
||||||
Description: This script converts a PDF file to WebP images. It includes functionality to resize images if they exceed specified dimensions and handle conversion of PDF pages to WebP format.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
To convert a PDF file to WebP images with each page as a separate WebP file:
|
|
||||||
python script.py input.pdf output_directory
|
|
||||||
|
|
||||||
To convert a PDF file to a single WebP image:
|
|
||||||
python script.py input.pdf output_directory --single
|
|
||||||
|
|
||||||
To adjust the DPI resolution for rendering PDF pages:
|
|
||||||
python script.py input.pdf output_directory --dpi 150
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
from pdf2image import convert_from_path
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
|
|
||||||
def resize_image(input_image_path, output_image_path, max_size=(16383, 16383)):
|
|
||||||
"""
|
|
||||||
Resize the image if its dimensions exceed the maximum allowed size and save it as WebP.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
input_image_path : str
|
|
||||||
Path to the input image file.
|
|
||||||
output_image_path : str
|
|
||||||
Path where the output WebP image will be saved.
|
|
||||||
max_size : tuple of int, optional
|
|
||||||
Maximum allowed dimensions for the image (width, height). Default is (16383, 16383).
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Open the image
|
|
||||||
image = Image.open(input_image_path)
|
|
||||||
width, height = image.size
|
|
||||||
max_width, max_height = max_size
|
|
||||||
|
|
||||||
# Check if the image dimensions exceed the maximum allowed dimensions
|
|
||||||
if width > max_width or height > max_height:
|
|
||||||
# Calculate the scaling ratio
|
|
||||||
ratio = min(max_width / width, max_height / height)
|
|
||||||
new_width = int(width * ratio)
|
|
||||||
new_height = int(height * ratio)
|
|
||||||
|
|
||||||
# Resize the image
|
|
||||||
resized_image = image.resize((new_width, new_height), Image.LANCZOS)
|
|
||||||
resized_image.save(output_image_path, format="WEBP", quality=100)
|
|
||||||
print(
|
|
||||||
f"The image was successfully resized to ({new_width}, {new_height}) and saved as WebP: {output_image_path}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# If dimensions are within the allowed limits, save the image directly
|
|
||||||
image.save(output_image_path, format="WEBP", quality=100)
|
|
||||||
print(f"The image was successfully saved as WebP: {output_image_path}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An error occurred: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
def convert_image_to_webp(input_image, output_file):
|
|
||||||
"""
|
|
||||||
Convert an image to WebP format, resizing it if it exceeds the maximum dimensions.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
input_image : str
|
|
||||||
Path to the input image file.
|
|
||||||
output_file : str
|
|
||||||
Path where the output WebP image will be saved.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
# Resize the image if it exceeds the maximum dimensions
|
|
||||||
resize_image(input_image, output_file, max_size=(16383, 16383))
|
|
||||||
|
|
||||||
|
|
||||||
def pdf_to_webp(pdf_path, output_dir, dpi=300):
|
|
||||||
"""
|
|
||||||
Convert each page of a PDF file to WebP images.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pdf_path : str
|
|
||||||
Path to the input PDF file.
|
|
||||||
output_dir : str
|
|
||||||
Directory where the WebP images will be saved.
|
|
||||||
dpi : int, optional
|
|
||||||
DPI resolution for rendering PDF pages. Default is 300.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
# Convert the PDF to a list of images
|
|
||||||
images = convert_from_path(pdf_path, dpi=dpi)
|
|
||||||
|
|
||||||
for page_number, image in enumerate(images):
|
|
||||||
# Define temporary PNG path
|
|
||||||
temp_png_path = os.path.join(output_dir, f"temp_page_{page_number + 1}.png")
|
|
||||||
image.save(temp_png_path, format="PNG")
|
|
||||||
|
|
||||||
# Define the output path for WebP
|
|
||||||
output_path = os.path.join(output_dir, f"page_{page_number + 1}.webp")
|
|
||||||
|
|
||||||
# Convert PNG to WebP
|
|
||||||
convert_image_to_webp(temp_png_path, output_path)
|
|
||||||
|
|
||||||
# Delete the temporary PNG file
|
|
||||||
os.remove(temp_png_path)
|
|
||||||
|
|
||||||
|
|
||||||
def main(pdf_image_path, output_dir, dpi=300, single_images_flag=False):
|
|
||||||
"""
|
|
||||||
Main function to handle conversion from PDF to WebP images.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
pdf_image_path : str
|
|
||||||
Path to the input PDF file or image.
|
|
||||||
output_dir : str
|
|
||||||
Directory where the WebP images will be saved.
|
|
||||||
dpi : int, optional
|
|
||||||
DPI resolution for rendering PDF pages. Default is 300.
|
|
||||||
single_images_flag : bool, optional
|
|
||||||
If True, combine all pages into a single WebP image. Default is False.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
if single_images_flag:
|
|
||||||
# Combine all pages into a single WebP image
|
|
||||||
output_path = os.path.join(output_dir, "combined_image.webp")
|
|
||||||
convert_image_to_webp(pdf_image_path, output_path)
|
|
||||||
else:
|
|
||||||
# Convert each PDF page to a separate WebP image
|
|
||||||
pdf_to_webp(pdf_image_path, output_dir, dpi)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(description="Convert a PDF file to WebP images.")
|
|
||||||
parser.add_argument("pdf_path", help="The path to the input PDF file.")
|
|
||||||
parser.add_argument(
|
|
||||||
"output_dir", help="The directory where the WebP images should be saved."
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dpi",
|
|
||||||
type=int,
|
|
||||||
default=300,
|
|
||||||
help="The DPI resolution for rendering the PDF pages (default: 300).",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--single",
|
|
||||||
action="store_true",
|
|
||||||
help="Combine all pages into a single WebP image.",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
os.makedirs(args.output_dir, exist_ok=True)
|
|
||||||
main(
|
|
||||||
args.pdf_path,
|
|
||||||
args.output_dir,
|
|
||||||
dpi=args.dpi,
|
|
||||||
single_images_flag=args.single,
|
|
||||||
)
|
|
||||||
@@ -45,6 +45,7 @@ public class SPdfApplication {
|
|||||||
// Check if the BROWSER_OPEN environment variable is set to true
|
// Check if the BROWSER_OPEN environment variable is set to true
|
||||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||||
|
|
||||||
if (browserOpen) {
|
if (browserOpen) {
|
||||||
try {
|
try {
|
||||||
String url = "http://localhost:" + getNonStaticPort();
|
String url = "http://localhost:" + getNonStaticPort();
|
||||||
@@ -65,7 +66,6 @@ public class SPdfApplication {
|
|||||||
public static void main(String[] args) throws IOException, InterruptedException {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||||
app.setAdditionalProfiles("default");
|
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
@@ -79,14 +79,13 @@ public class SPdfApplication {
|
|||||||
|
|
||||||
// custom javs settings file
|
// custom javs settings file
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
||||||
String existingLocation =
|
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
if (!existing.isEmpty()) {
|
||||||
if (!existingLocation.isEmpty()) {
|
existing += ",";
|
||||||
existingLocation += ",";
|
|
||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existingLocation + "file:configs/custom_settings.yml");
|
existing + "file:configs/custom_settings.yml");
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,12 +126,13 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "directoryFilter")
|
@Bean(name = "directoryFilter")
|
||||||
public Predicate<Path> processOnlyFiles() {
|
public Predicate<Path> processPDFOnlyFilter() {
|
||||||
return path -> {
|
return path -> {
|
||||||
if (Files.isDirectory(path)) {
|
if (Files.isDirectory(path)) {
|
||||||
return !path.toString().contains("processing");
|
return !path.toString().contains("processing");
|
||||||
} else {
|
} else {
|
||||||
return true;
|
String fileName = path.getFileName().toString();
|
||||||
|
return fileName.endsWith(".pdf");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,25 +32,25 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
String queryString = request.getQueryString();
|
String queryString = request.getQueryString();
|
||||||
if (queryString != null && !queryString.isEmpty()) {
|
if (queryString != null && !queryString.isEmpty()) {
|
||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Map<String, String> allowedParameters = new HashMap<>();
|
Map<String, String> parameters = new HashMap<>();
|
||||||
|
|
||||||
// Keep only the allowed parameters
|
// Keep only the allowed parameters
|
||||||
String[] queryParameters = queryString.split("&");
|
String[] queryParameters = queryString.split("&");
|
||||||
for (String param : queryParameters) {
|
for (String param : queryParameters) {
|
||||||
String[] keyValuePair = param.split("=");
|
String[] keyValue = param.split("=");
|
||||||
if (keyValuePair.length != 2) {
|
if (keyValue.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ALLOWED_PARAMS.contains(keyValuePair[0])) {
|
if (ALLOWED_PARAMS.contains(keyValue[0])) {
|
||||||
allowedParameters.put(keyValuePair[0], keyValuePair[1]);
|
parameters.put(keyValue[0], keyValue[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are any parameters that are not allowed
|
// If there are any parameters that are not allowed
|
||||||
if (allowedParameters.size() != queryParameters.length) {
|
if (parameters.size() != queryParameters.length) {
|
||||||
// Construct new query string
|
// Construct new query string
|
||||||
StringBuilder newQueryString = new StringBuilder();
|
StringBuilder newQueryString = new StringBuilder();
|
||||||
for (Map.Entry<String, String> entry : allowedParameters.entrySet()) {
|
for (Map.Entry<String, String> entry : parameters.entrySet()) {
|
||||||
if (newQueryString.length() > 0) {
|
if (newQueryString.length() > 0) {
|
||||||
newQueryString.append("&");
|
newQueryString.append("&");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.simpleyaml.configuration.comments.CommentType;
|
import org.simpleyaml.configuration.comments.CommentType;
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
|
||||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
@@ -73,17 +71,9 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile());
|
||||||
DumperOptions yamlOptionsSettingsTemplateFile =
|
|
||||||
((SimpleYamlImplementation) settingsTemplateFile.getImplementation())
|
|
||||||
.getDumperOptions();
|
|
||||||
yamlOptionsSettingsTemplateFile.setSplitLines(false);
|
|
||||||
settingsTemplateFile.loadWithComments();
|
settingsTemplateFile.loadWithComments();
|
||||||
|
|
||||||
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
final YamlFile settingsFile = new YamlFile(settingsPath.toFile());
|
||||||
DumperOptions yamlOptionsSettingsFile =
|
|
||||||
((SimpleYamlImplementation) settingsFile.getImplementation())
|
|
||||||
.getDumperOptions();
|
|
||||||
yamlOptionsSettingsFile.setSplitLines(false);
|
|
||||||
settingsFile.loadWithComments();
|
settingsFile.loadWithComments();
|
||||||
|
|
||||||
// Load headers and comments
|
// Load headers and comments
|
||||||
@@ -91,10 +81,6 @@ public class ConfigInitializer
|
|||||||
|
|
||||||
// Create a new file for temporary settings
|
// Create a new file for temporary settings
|
||||||
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile());
|
||||||
DumperOptions yamlOptionsTempSettingFile =
|
|
||||||
((SimpleYamlImplementation) tempSettingFile.getImplementation())
|
|
||||||
.getDumperOptions();
|
|
||||||
yamlOptionsTempSettingFile.setSplitLines(false);
|
|
||||||
tempSettingFile.createNewFile(true);
|
tempSettingFile.createNewFile(true);
|
||||||
tempSettingFile.setHeader(header);
|
tempSettingFile.setHeader(header);
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", "auto-rename");
|
addEndpointToGroup("Other", "auto-rename");
|
||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
addEndpointToGroup("Other", "remove-image-pdf");
|
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
@@ -166,7 +165,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Python", REMOVE_BLANKS);
|
addEndpointToGroup("Python", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Python", "html-to-pdf");
|
addEndpointToGroup("Python", "html-to-pdf");
|
||||||
addEndpointToGroup("Python", "url-to-pdf");
|
addEndpointToGroup("Python", "url-to-pdf");
|
||||||
addEndpointToGroup("Python", "pdf-to-img");
|
|
||||||
|
|
||||||
// openCV
|
// openCV
|
||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
@@ -223,7 +221,6 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "split-pdf-by-sections");
|
addEndpointToGroup("Java", "split-pdf-by-sections");
|
||||||
addEndpointToGroup("Java", REMOVE_BLANKS);
|
addEndpointToGroup("Java", REMOVE_BLANKS);
|
||||||
addEndpointToGroup("Java", "pdf-to-text");
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
addEndpointToGroup("Java", "remove-image-pdf");
|
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.authentication.DisabledException;
|
|
||||||
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
import org.springframework.security.authentication.InternalAuthenticationServiceException;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
@@ -14,16 +15,17 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class);
|
||||||
|
|
||||||
public CustomAuthenticationFailureHandler(
|
public CustomAuthenticationFailureHandler(
|
||||||
final LoginAttemptService loginAttemptService, UserService userService) {
|
final LoginAttemptService loginAttemptService, UserService userService) {
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
@@ -37,17 +39,14 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
if (exception instanceof DisabledException) {
|
|
||||||
log.error("User is deactivated: ", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String ip = request.getRemoteAddr();
|
String ip = request.getRemoteAddr();
|
||||||
log.error("Failed login attempt from IP: {}", ip);
|
logger.error("Failed login attempt from IP: {}", ip);
|
||||||
|
|
||||||
if (exception instanceof LockedException) {
|
String contextPath = request.getContextPath();
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
|
||||||
|
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class)
|
||||||
|
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
||||||
|
response.sendRedirect(contextPath + "/login?error=oauth2AuthenticationError");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,25 +54,20 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
Optional<User> optUser = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
if (username != null && optUser.isPresent() && !isDemoUser(optUser)) {
|
||||||
log.info(
|
logger.info(
|
||||||
"Remaining attempts for user {}: {}",
|
"Remaining attempts for user {}: {}",
|
||||||
username,
|
optUser.get().getUsername(),
|
||||||
loginAttemptService.getRemainingAttempts(username));
|
loginAttemptService.getRemainingAttempts(username));
|
||||||
loginAttemptService.loginFailed(username);
|
loginAttemptService.loginFailed(username);
|
||||||
if (loginAttemptService.isBlocked(username) || exception instanceof LockedException) {
|
if (loginAttemptService.isBlocked(username)
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=locked");
|
|| exception.getClass().isAssignableFrom(LockedException.class)) {
|
||||||
|
response.sendRedirect(contextPath + "/login?error=locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exception instanceof BadCredentialsException
|
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|
||||||
|| exception instanceof UsernameNotFoundException) {
|
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
response.sendRedirect(contextPath + "/login?error=badcredentials");
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof InternalAuthenticationServiceException
|
|
||||||
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
|
|
||||||
getRedirectStrategy()
|
|
||||||
.sendRedirect(request, response, "/login?error=oauth2AuthenticationError");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,20 +10,15 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomAuthenticationSuccessHandler
|
public class CustomAuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
public CustomAuthenticationSuccessHandler(
|
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
|
||||||
LoginAttemptService loginAttemptService, UserService userService) {
|
|
||||||
this.loginAttemptService = loginAttemptService;
|
this.loginAttemptService = loginAttemptService;
|
||||||
this.userService = userService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -32,10 +27,6 @@ public class CustomAuthenticationSuccessHandler
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
String userName = request.getParameter("username");
|
String userName = request.getParameter("username");
|
||||||
if (userService.isUserDisabled(userName)) {
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loginAttemptService.loginSucceeded(userName);
|
loginAttemptService.loginSucceeded(userName);
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
|
|||||||
@@ -2,26 +2,32 @@ package stirling.software.SPDF.config.security;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
@Autowired SessionRegistry sessionRegistry;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
if (request.getParameter("userIsDisabled") != null) {
|
if (session != null) {
|
||||||
getRedirectStrategy()
|
String sessionId = session.getId();
|
||||||
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
return;
|
session.invalidate();
|
||||||
|
logger.debug("Session invalidated: " + sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
response.sendRedirect(request.getContextPath() + "/login?logout=true");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import java.nio.file.Paths;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
|
||||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -94,9 +92,6 @@ public class InitialSecuritySetup {
|
|||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
DumperOptions yamlOptionssettingsYml =
|
|
||||||
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
|
||||||
yamlOptionssettingsYml.setSplitLines(false);
|
|
||||||
|
|
||||||
settingsYml.loadWithComments();
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
|||||||
@@ -3,32 +3,29 @@ package stirling.software.SPDF.config.security;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.AttemptCounter;
|
import stirling.software.SPDF.model.AttemptCounter;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
|
||||||
public class LoginAttemptService {
|
public class LoginAttemptService {
|
||||||
|
|
||||||
@Autowired private ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class);
|
||||||
|
|
||||||
private int MAX_ATTEMPT;
|
private int MAX_ATTEMPT;
|
||||||
private long ATTEMPT_INCREMENT_TIME;
|
private long ATTEMPT_INCREMENT_TIME;
|
||||||
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
|
||||||
private boolean isBlockedEnabled = true;
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
|
||||||
if (MAX_ATTEMPT == -1) {
|
|
||||||
isBlockedEnabled = false;
|
|
||||||
log.info("Login attempt tracking is disabled.");
|
|
||||||
}
|
|
||||||
ATTEMPT_INCREMENT_TIME =
|
ATTEMPT_INCREMENT_TIME =
|
||||||
TimeUnit.MINUTES.toMillis(
|
TimeUnit.MINUTES.toMillis(
|
||||||
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
@@ -36,16 +33,14 @@ public class LoginAttemptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void loginSucceeded(String key) {
|
public void loginSucceeded(String key) {
|
||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (key == null || key.trim().isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
attemptsCache.remove(key.toLowerCase());
|
attemptsCache.remove(key.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loginFailed(String key) {
|
public void loginFailed(String key) {
|
||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (key == null || key.trim().isEmpty()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
@@ -60,9 +55,7 @@ public class LoginAttemptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBlocked(String key) {
|
public boolean isBlocked(String key) {
|
||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (key == null || key.trim().isEmpty()) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -72,9 +65,7 @@ public class LoginAttemptService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getRemainingAttempts(String key) {
|
public int getRemainingAttempts(String key) {
|
||||||
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
|
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT;
|
||||||
return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled
|
|
||||||
}
|
|
||||||
|
|
||||||
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
|
||||||
if (attemptCounter == null) {
|
if (attemptCounter == null) {
|
||||||
|
|||||||
@@ -6,20 +6,27 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
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.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.Customizer;
|
|
||||||
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;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
||||||
|
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
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;
|
||||||
@@ -30,29 +37,22 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationF
|
|||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler;
|
|
||||||
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler;
|
|
||||||
import stirling.software.SPDF.config.security.saml.SAMLLogoutSuccessHandler;
|
|
||||||
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.Client;
|
||||||
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity()
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private GrantedAuthoritiesMapper userAuthoritiesMapper;
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private OpenSaml4AuthenticationProvider samlAuthenticationProvider;
|
|
||||||
|
|
||||||
@Autowired(required = false)
|
|
||||||
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -73,7 +73,11 @@ public class SecurityConfiguration {
|
|||||||
@Autowired private LoginAttemptService loginAttemptService;
|
@Autowired private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
@Autowired private SessionPersistentRegistry sessionRegistry;
|
|
||||||
|
@Bean
|
||||||
|
public SessionRegistry sessionRegistry() {
|
||||||
|
return new SessionRegistryImpl();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
@@ -90,7 +94,7 @@ public class SecurityConfiguration {
|
|||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
.maximumSessions(10)
|
.maximumSessions(10)
|
||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry())
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
|
|
||||||
http.formLogin(
|
http.formLogin(
|
||||||
@@ -99,7 +103,7 @@ public class SecurityConfiguration {
|
|||||||
.loginPage("/login")
|
.loginPage("/login")
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomAuthenticationSuccessHandler(
|
new CustomAuthenticationSuccessHandler(
|
||||||
loginAttemptService, userService))
|
loginAttemptService))
|
||||||
.defaultSuccessUrl("/")
|
.defaultSuccessUrl("/")
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomAuthenticationFailureHandler(
|
new CustomAuthenticationFailureHandler(
|
||||||
@@ -137,7 +141,6 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
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")
|
||||||
@@ -152,15 +155,12 @@ public class SecurityConfiguration {
|
|||||||
})
|
})
|
||||||
.permitAll()
|
.permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.authenticated());
|
.authenticated())
|
||||||
|
.authenticationProvider(authenticationProvider());
|
||||||
|
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().getOAUTH2() != null
|
if (applicationProperties.getSecurity().getOAUTH2() != null
|
||||||
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()
|
&& applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
|
||||||
&& !applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getLoginMethod()
|
|
||||||
.equalsIgnoreCase("normal")) {
|
|
||||||
|
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
@@ -187,41 +187,14 @@ public class SecurityConfiguration {
|
|||||||
userService,
|
userService,
|
||||||
loginAttemptService))
|
loginAttemptService))
|
||||||
.userAuthoritiesMapper(
|
.userAuthoritiesMapper(
|
||||||
userAuthoritiesMapper)))
|
userAuthoritiesMapper())))
|
||||||
.logout(
|
.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutSuccessHandler(
|
logout.logoutSuccessHandler(
|
||||||
new CustomOAuth2LogoutSuccessHandler(
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
applicationProperties)));
|
this.applicationProperties,
|
||||||
}
|
sessionRegistry()))
|
||||||
|
.invalidateHttpSession(true));
|
||||||
// Handle SAML Logins
|
|
||||||
if (applicationProperties.getSecurity().getSAML() != null
|
|
||||||
&& applicationProperties.getSecurity().getSAML().getEnabled()
|
|
||||||
&& !applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getLoginMethod()
|
|
||||||
.equalsIgnoreCase("normal")) {
|
|
||||||
|
|
||||||
http.saml2Login(
|
|
||||||
saml2 ->
|
|
||||||
saml2.relyingPartyRegistrationRepository(
|
|
||||||
relyingPartyRegistrationRepository)
|
|
||||||
.loginProcessingUrl("/login/saml2/sso/stirling")
|
|
||||||
.loginPage("/saml2")
|
|
||||||
.authenticationManager(
|
|
||||||
new ProviderManager(
|
|
||||||
samlAuthenticationProvider))
|
|
||||||
.successHandler(
|
|
||||||
new CustomSAMLAuthenticationSuccessHandler(
|
|
||||||
loginAttemptService, userService))
|
|
||||||
.failureHandler(
|
|
||||||
new CustomSAMLAuthenticationFailureHandler()))
|
|
||||||
.saml2Metadata(Customizer.withDefaults())
|
|
||||||
.logout(
|
|
||||||
logout ->
|
|
||||||
logout.logoutSuccessHandler(
|
|
||||||
new SAMLLogoutSuccessHandler()));
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
@@ -231,12 +204,192 @@ public class SecurityConfiguration {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client Registration Repository for OAUTH2 OIDC Login
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||||
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||||||
|
|
||||||
|
githubClientRegistration().ifPresent(registrations::add);
|
||||||
|
oidcClientRegistration().ifPresent(registrations::add);
|
||||||
|
googleClientRegistration().ifPresent(registrations::add);
|
||||||
|
keycloakClientRegistration().ifPresent(registrations::add);
|
||||||
|
|
||||||
|
if (registrations.isEmpty()) {
|
||||||
|
logger.error("At least one OAuth2 provider must be configured");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InMemoryClientRegistrationRepository(registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> googleClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GoogleProvider google = client.getGoogle();
|
||||||
|
return google != null && google.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistration.withRegistrationId(google.getName())
|
||||||
|
.clientId(google.getClientId())
|
||||||
|
.clientSecret(google.getClientSecret())
|
||||||
|
.scope(google.getScopes())
|
||||||
|
.authorizationUri(google.getAuthorizationuri())
|
||||||
|
.tokenUri(google.getTokenuri())
|
||||||
|
.userInfoUri(google.getUserinfouri())
|
||||||
|
.userNameAttributeName(google.getUseAsUsername())
|
||||||
|
.clientName(google.getClientName())
|
||||||
|
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
||||||
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> keycloakClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
|
|
||||||
|
return keycloak != null && keycloak.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||||
|
.registrationId(keycloak.getName())
|
||||||
|
.clientId(keycloak.getClientId())
|
||||||
|
.clientSecret(keycloak.getClientSecret())
|
||||||
|
.scope(keycloak.getScopes())
|
||||||
|
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||||
|
.clientName(keycloak.getClientName())
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null || !oauth.getEnabled()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
Client client = oauth.getClient();
|
||||||
|
if (client == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
GithubProvider github = client.getGithub();
|
||||||
|
return github != null && github.isSettingsValid()
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistration.withRegistrationId(github.getName())
|
||||||
|
.clientId(github.getClientId())
|
||||||
|
.clientSecret(github.getClientSecret())
|
||||||
|
.scope(github.getScopes())
|
||||||
|
.authorizationUri(github.getAuthorizationuri())
|
||||||
|
.tokenUri(github.getTokenuri())
|
||||||
|
.userInfoUri(github.getUserinfouri())
|
||||||
|
.userNameAttributeName(github.getUseAsUsername())
|
||||||
|
.clientName(github.getClientName())
|
||||||
|
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
||||||
|
.authorizationGrantType(
|
||||||
|
org.springframework.security.oauth2.core
|
||||||
|
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> oidcClientRegistration() {
|
||||||
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
if (oauth == null
|
||||||
|
|| oauth.getIssuer() == null
|
||||||
|
|| oauth.getIssuer().isEmpty()
|
||||||
|
|| oauth.getClientId() == null
|
||||||
|
|| oauth.getClientId().isEmpty()
|
||||||
|
|| oauth.getClientSecret() == null
|
||||||
|
|| oauth.getClientSecret().isEmpty()
|
||||||
|
|| oauth.getScopes() == null
|
||||||
|
|| oauth.getScopes().isEmpty()
|
||||||
|
|| oauth.getUseAsUsername() == null
|
||||||
|
|| oauth.getUseAsUsername().isEmpty()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return Optional.of(
|
||||||
|
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||||
|
.registrationId("oidc")
|
||||||
|
.clientId(oauth.getClientId())
|
||||||
|
.clientSecret(oauth.getClientSecret())
|
||||||
|
.scope(oauth.getScopes())
|
||||||
|
.userNameAttributeName(oauth.getUseAsUsername())
|
||||||
|
.clientName("OIDC")
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
|
return (authorities) -> {
|
||||||
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
|
|
||||||
|
authorities.forEach(
|
||||||
|
authority -> {
|
||||||
|
// Add existing OAUTH2 Authorities
|
||||||
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
|
|
||||||
|
// Add Authorities from database for existing user, if user is present.
|
||||||
|
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
||||||
|
String useAsUsername =
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getOAUTH2()
|
||||||
|
.getUseAsUsername();
|
||||||
|
Optional<User> userOpt =
|
||||||
|
userService.findByUsernameIgnoreCase(
|
||||||
|
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
||||||
|
if (userOpt.isPresent()) {
|
||||||
|
User user = userOpt.get();
|
||||||
|
if (user != null) {
|
||||||
|
mappedAuthorities.add(
|
||||||
|
new SimpleGrantedAuthority(
|
||||||
|
userService.findRole(user).getAuthority()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mappedAuthorities;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public IPRateLimitingFilter rateLimitingFilter() {
|
public IPRateLimitingFilter rateLimitingFilter() {
|
||||||
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
|
||||||
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
|
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||||
|
authProvider.setUserDetailsService(userDetailsService);
|
||||||
|
authProvider.setPasswordEncoder(passwordEncoder());
|
||||||
|
return authProvider;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PersistentTokenRepository persistentTokenRepository() {
|
public PersistentTokenRepository persistentTokenRepository() {
|
||||||
return new JPATokenRepositoryImpl();
|
return new JPATokenRepositoryImpl();
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -11,11 +8,9 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
@@ -23,16 +18,14 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
import stirling.software.SPDF.model.User;
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
@Autowired private UserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
@Autowired @Lazy private UserService userService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
@@ -58,20 +51,15 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
try {
|
try {
|
||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
// provider for API keys.
|
// provider for API keys.
|
||||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
UserDetails userDetails = userService.loadUserByApiKey(apiKey);
|
||||||
if (!user.isPresent()) {
|
if (userDetails == null) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<SimpleGrantedAuthority> authorities =
|
authentication =
|
||||||
user.get().getAuthorities().stream()
|
new ApiKeyAuthenticationToken(
|
||||||
.map(
|
userDetails, apiKey, userDetails.getAuthorities());
|
||||||
authority ->
|
|
||||||
new SimpleGrantedAuthority(
|
|
||||||
authority.getAuthority()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
|
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
} catch (AuthenticationException e) {
|
} catch (AuthenticationException e) {
|
||||||
// If API key authentication fails, deny the request
|
// If API key authentication fails, deny the request
|
||||||
@@ -99,43 +87,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the authenticated user is disabled and invalidate their session if so
|
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
|
||||||
Object principal = authentication.getPrincipal();
|
|
||||||
String username = null;
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
username = ((UserDetails) principal).getUsername();
|
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
username = ((OAuth2User) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
|
||||||
username = (String) principal;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<SessionInformation> sessionsInformations =
|
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
|
||||||
|
|
||||||
if (username != null) {
|
|
||||||
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
|
||||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
|
||||||
|
|
||||||
if (!isUserExists || isUserDisabled) {
|
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
|
||||||
sessionsInformation.expireNow();
|
|
||||||
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isUserExists) {
|
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isUserDisabled) {
|
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -11,15 +15,12 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
import stirling.software.SPDF.model.Authority;
|
import stirling.software.SPDF.model.Authority;
|
||||||
@@ -39,8 +40,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired private MessageSource messageSource;
|
@Autowired private MessageSource messageSource;
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionRegistry;
|
|
||||||
|
|
||||||
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
@Autowired DatabaseBackupInterface databaseBackupHelper;
|
||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
@@ -49,7 +48,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -61,8 +60,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
Optional<User> user = getUserByApiKey(apiKey);
|
User user = getUserByApiKey(apiKey);
|
||||||
if (!user.isPresent()) {
|
if (user == null) {
|
||||||
throw new UsernameNotFoundException("API key is not valid");
|
throw new UsernameNotFoundException("API key is not valid");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
return new UsernamePasswordAuthenticationToken(
|
return new UsernamePasswordAuthenticationToken(
|
||||||
user, // principal (typically the user)
|
user, // principal (typically the user)
|
||||||
null, // credentials (we don't expose the password or API key here)
|
null, // credentials (we don't expose the password or API key here)
|
||||||
getAuthorities(user.get()) // user's authorities (roles/permissions)
|
getAuthorities(user) // user's authorities (roles/permissions)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,17 +84,18 @@ public class UserService implements UserServiceInterface {
|
|||||||
String apiKey;
|
String apiKey;
|
||||||
do {
|
do {
|
||||||
apiKey = UUID.randomUUID().toString();
|
apiKey = UUID.randomUUID().toString();
|
||||||
} while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
|
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
|
||||||
return apiKey;
|
return apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
Optional<User> user = findByUsernameIgnoreCase(username);
|
User user =
|
||||||
if (user.isPresent()) {
|
userRepository
|
||||||
user.get().setApiKey(generateApiKey());
|
.findByUsernameIgnoreCase(username)
|
||||||
return userRepository.save(user.get());
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
}
|
|
||||||
throw new UsernameNotFoundException("User not found");
|
user.setApiKey(generateApiKey());
|
||||||
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
@@ -104,40 +104,39 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public String getApiKeyForUser(String username) {
|
public String getApiKeyForUser(String username) {
|
||||||
User user =
|
User user =
|
||||||
findByUsernameIgnoreCase(username)
|
userRepository
|
||||||
|
.findByUsernameIgnoreCase(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isValidApiKey(String apiKey) {
|
public boolean isValidApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey).isPresent();
|
return userRepository.findByApiKey(apiKey) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> getUserByApiKey(String apiKey) {
|
public User getUserByApiKey(String apiKey) {
|
||||||
return userRepository.findByApiKey(apiKey);
|
return userRepository.findByApiKey(apiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> loadUserByApiKey(String apiKey) {
|
public UserDetails loadUserByApiKey(String apiKey) {
|
||||||
Optional<User> user = userRepository.findByApiKey(apiKey);
|
User user = userRepository.findByApiKey(apiKey);
|
||||||
|
if (user != null) {
|
||||||
if (user.isPresent()) {
|
// Convert your User entity to a UserDetails object with authorities
|
||||||
return user;
|
return new org.springframework.security.core.userdetails.User(
|
||||||
|
user.getUsername(),
|
||||||
|
user.getPassword(), // you might not need this for API key auth
|
||||||
|
getAuthorities(user));
|
||||||
}
|
}
|
||||||
return null; // or throw an exception
|
return null; // or throw an exception
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateApiKeyForUser(String username, String apiKey) {
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType)
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
|
||||||
throws IllegalArgumentException, IOException {
|
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
@@ -145,7 +144,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setUsername(username);
|
user.setUsername(username);
|
||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setFirstLogin(false);
|
user.setFirstLogin(false);
|
||||||
user.addAuthority(new Authority(role, user));
|
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
|
||||||
user.setAuthenticationType(authenticationType);
|
user.setAuthenticationType(authenticationType);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
@@ -187,7 +186,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void deleteUser(String username) {
|
public void deleteUser(String username) {
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
for (Authority authority : userOpt.get().getAuthorities()) {
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
||||||
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
@@ -196,20 +195,21 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
userRepository.delete(userOpt.get());
|
userRepository.delete(userOpt.get());
|
||||||
}
|
}
|
||||||
invalidateUserSessions(username);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExists(String username) {
|
public boolean usernameExists(String username) {
|
||||||
return findByUsername(username).isPresent();
|
return userRepository.findByUsername(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean usernameExistsIgnoreCase(String username) {
|
public boolean usernameExistsIgnoreCase(String username) {
|
||||||
return findByUsernameIgnoreCase(username).isPresent();
|
return userRepository.findByUsernameIgnoreCase(username).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasUsers() {
|
public boolean hasUsers() {
|
||||||
long userCount = userRepository.count();
|
long userCount = userRepository.count();
|
||||||
if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
|
if (userRepository
|
||||||
|
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
|
||||||
|
.isPresent()) {
|
||||||
userCount -= 1;
|
userCount -= 1;
|
||||||
}
|
}
|
||||||
return userCount > 0;
|
return userCount > 0;
|
||||||
@@ -217,7 +217,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public void updateUserSettings(String username, Map<String, String> updates)
|
public void updateUserSettings(String username, Map<String, String> updates)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
Map<String, String> settingsMap = user.getSettings();
|
Map<String, String> settingsMap = user.getSettings();
|
||||||
@@ -242,10 +242,6 @@ public class UserService implements UserServiceInterface {
|
|||||||
return userRepository.findByUsernameIgnoreCase(username);
|
return userRepository.findByUsernameIgnoreCase(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Optional<User> findByUsernameIgnoreCaseWithSettings(String username) {
|
|
||||||
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Authority findRole(User user) {
|
public Authority findRole(User user) {
|
||||||
return authorityRepository.findByUserId(user.getId());
|
return authorityRepository.findByUserId(user.getId());
|
||||||
}
|
}
|
||||||
@@ -272,17 +268,10 @@ public class UserService implements UserServiceInterface {
|
|||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeRole(User user, String newRole) throws IOException {
|
public void changeRole(User user, String newRole) {
|
||||||
Authority userAuthority = this.findRole(user);
|
Authority userAuthority = this.findRole(user);
|
||||||
userAuthority.setAuthority(newRole);
|
userAuthority.setAuthority(newRole);
|
||||||
authorityRepository.save(userAuthority);
|
authorityRepository.save(userAuthority);
|
||||||
databaseBackupHelper.exportDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
|
|
||||||
user.setEnabled(enbeled);
|
|
||||||
userRepository.save(user);
|
|
||||||
databaseBackupHelper.exportDatabase();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
@@ -306,40 +295,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasPassword(String username) {
|
public boolean hasPassword(String username) {
|
||||||
Optional<User> user = findByUsernameIgnoreCase(username);
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent() && user.get().hasPassword();
|
return user.isPresent() && user.get().hasPassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isAuthenticationTypeByUsername(
|
public boolean isAuthenticationTypeByUsername(
|
||||||
String username, AuthenticationType authenticationType) {
|
String username, AuthenticationType authenticationType) {
|
||||||
Optional<User> user = findByUsernameIgnoreCase(username);
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
return user.isPresent()
|
return user.isPresent()
|
||||||
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUserDisabled(String username) {
|
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
|
||||||
return userOpt.map(user -> !user.isEnabled()).orElse(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void invalidateUserSessions(String username) {
|
|
||||||
String usernameP = "";
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
|
||||||
for (SessionInformation sessionsInformation :
|
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
|
||||||
usernameP = userDetails.getUsername();
|
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
OAuth2User oAuth2User = (OAuth2User) principal;
|
|
||||||
usernameP = oAuth2User.getName();
|
|
||||||
} else if (principal instanceof String) {
|
|
||||||
usernameP = (String) principal;
|
|
||||||
}
|
|
||||||
if (usernameP.equalsIgnoreCase(username)) {
|
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,10 +163,6 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
|
|
||||||
// Deletes a backup file.
|
// Deletes a backup file.
|
||||||
public boolean deleteBackupFile(String fileName) throws IOException {
|
public boolean deleteBackupFile(String fileName) throws IOException {
|
||||||
if (!isValidFileName(fileName)) {
|
|
||||||
log.error("Invalid file name: {}", fileName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Path filePath = this.getBackupFilePath(fileName);
|
Path filePath = this.getBackupFilePath(fileName);
|
||||||
if (Files.deleteIfExists(filePath)) {
|
if (Files.deleteIfExists(filePath)) {
|
||||||
log.info("Deleted backup file: {}", fileName);
|
log.info("Deleted backup file: {}", fileName);
|
||||||
@@ -179,20 +175,15 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
|
|
||||||
// Gets the Path object for a given backup file name.
|
// Gets the Path object for a given backup file name.
|
||||||
public Path getBackupFilePath(String fileName) {
|
public Path getBackupFilePath(String fileName) {
|
||||||
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
|
return Paths.get(backupPath.toString(), fileName);
|
||||||
if (!filePath.startsWith(backupPath)) {
|
|
||||||
throw new SecurityException("Path traversal detected");
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean executeDatabaseScript(Path scriptPath) {
|
private boolean executeDatabaseScript(Path scriptPath) {
|
||||||
String query = "RUNSCRIPT from ?;";
|
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
Statement stmt = conn.createStatement()) {
|
||||||
stmt.setString(1, scriptPath.toString());
|
|
||||||
stmt.execute();
|
String query = "RUNSCRIPT from '" + scriptPath.toString() + "';";
|
||||||
|
stmt.execute(query);
|
||||||
log.info("Database import completed: {}", scriptPath);
|
log.info("Database import completed: {}", scriptPath);
|
||||||
return true;
|
return true;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -210,19 +201,4 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidFileName(String fileName) {
|
|
||||||
// Check for invalid characters or sequences
|
|
||||||
return fileName != null
|
|
||||||
&& !fileName.contains("..")
|
|
||||||
&& !fileName.contains("/")
|
|
||||||
&& !fileName.contains("\\")
|
|
||||||
&& !fileName.contains(":")
|
|
||||||
&& !fileName.contains("*")
|
|
||||||
&& !fileName.contains("?")
|
|
||||||
&& !fileName.contains("\"")
|
|
||||||
&& !fileName.contains("<")
|
|
||||||
&& !fileName.contains(">")
|
|
||||||
&& !fileName.contains("|");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.slf4j.Logger;
|
||||||
import org.springframework.security.authentication.DisabledException;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
|
||||||
@@ -13,34 +13,19 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomOAuth2AuthenticationFailureHandler
|
public class CustomOAuth2AuthenticationFailureHandler
|
||||||
extends SimpleUrlAuthenticationFailureHandler {
|
extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(
|
public void onAuthenticationFailure(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
|
|
||||||
if (exception instanceof BadCredentialsException) {
|
|
||||||
log.error("BadCredentialsException", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof DisabledException) {
|
|
||||||
log.error("User is deactivated: ", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof LockedException) {
|
|
||||||
log.error("Account locked: ", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof OAuth2AuthenticationException) {
|
if (exception instanceof OAuth2AuthenticationException) {
|
||||||
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
||||||
|
|
||||||
@@ -49,13 +34,17 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
if (error.getErrorCode().equals("Password must not be null")) {
|
if (error.getErrorCode().equals("Password must not be null")) {
|
||||||
errorCode = "userAlreadyExistsWeb";
|
errorCode = "userAlreadyExistsWeb";
|
||||||
}
|
}
|
||||||
log.error("OAuth2 Authentication error: " + errorCode);
|
logger.error("OAuth2 Authentication error: " + errorCode);
|
||||||
log.error("OAuth2AuthenticationException", exception);
|
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
|
||||||
return;
|
return;
|
||||||
|
} else if (exception instanceof LockedException) {
|
||||||
|
logger.error("Account locked: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
logger.error("Unhandled authentication exception", exception);
|
||||||
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
}
|
}
|
||||||
log.error("Unhandled authentication exception", exception);
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
@@ -25,6 +26,9 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
private ApplicationProperties applicationProperties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@@ -42,17 +46,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
Object principal = authentication.getPrincipal();
|
|
||||||
String username = "";
|
|
||||||
|
|
||||||
if (principal instanceof OAuth2User) {
|
|
||||||
OAuth2User oauthUser = (OAuth2User) principal;
|
|
||||||
username = oauthUser.getName();
|
|
||||||
} else if (principal instanceof UserDetails) {
|
|
||||||
UserDetails oauthUser = (UserDetails) principal;
|
|
||||||
username = oauthUser.getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
@@ -66,8 +59,11 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
// Redirect to the original destination
|
// Redirect to the original destination
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
|
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
|
||||||
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
|
String username = oauthUser.getName();
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
@@ -82,21 +78,15 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
} else {
|
||||||
try {
|
try {
|
||||||
if (oAuth.getBlockRegistration()
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
response.sendRedirect(contextPath + "/");
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
|
||||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
|
||||||
}
|
|
||||||
response.sendRedirect(contextPath + "/");
|
|
||||||
return;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,34 @@ package stirling.software.SPDF.config.security.oauth2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.Provider;
|
import stirling.software.SPDF.model.Provider;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
private static final Logger logger =
|
||||||
|
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
|
||||||
|
|
||||||
|
private final SessionRegistry sessionRegistry;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
|
public CustomOAuth2LogoutSuccessHandler(
|
||||||
|
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
|
||||||
|
this.sessionRegistry = sessionRegistry;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,15 +42,6 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
String issuer = null;
|
String issuer = null;
|
||||||
String clientId = null;
|
String clientId = null;
|
||||||
|
|
||||||
if (authentication == null) {
|
|
||||||
if (request.getParameter("userIsDisabled") != null) {
|
|
||||||
response.sendRedirect(
|
|
||||||
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
|
||||||
} else {
|
|
||||||
super.onLogoutSuccess(request, response, authentication);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
||||||
|
|
||||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||||
@@ -54,8 +53,9 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
issuer = provider.getIssuer();
|
issuer = provider.getIssuer();
|
||||||
clientId = provider.getClientId();
|
clientId = provider.getClientId();
|
||||||
} catch (UnsupportedProviderException e) {
|
} catch (UnsupportedProviderException e) {
|
||||||
log.error(e.getMessage());
|
logger.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||||
issuer = oauth.getIssuer();
|
issuer = oauth.getIssuer();
|
||||||
@@ -70,16 +70,18 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
param = "error=oauth2AutoCreateDisabled";
|
||||||
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
|
||||||
param = "erroroauth=oauth2_admin_blocked_user";
|
|
||||||
} else if (request.getParameter("userIsDisabled") != null) {
|
|
||||||
param = "erroroauth=userIsDisabled";
|
|
||||||
} else if (request.getParameter("badcredentials") != null) {
|
|
||||||
param = "error=badcredentials";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||||
|
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session != null) {
|
||||||
|
String sessionId = session.getId();
|
||||||
|
sessionRegistry.removeSessionInformation(sessionId);
|
||||||
|
session.invalidate();
|
||||||
|
logger.info("Session invalidated: " + sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak":
|
case "keycloak":
|
||||||
// Add Keycloak specific logout URL if needed
|
// Add Keycloak specific logout URL if needed
|
||||||
@@ -90,13 +92,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
+ clientId
|
+ clientId
|
||||||
+ "&post_logout_redirect_uri="
|
+ "&post_logout_redirect_uri="
|
||||||
+ response.encodeRedirectURL(redirect_url);
|
+ response.encodeRedirectURL(redirect_url);
|
||||||
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
logger.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
break;
|
break;
|
||||||
case "github":
|
case "github":
|
||||||
// Add GitHub specific logout URL if needed
|
// Add GitHub specific logout URL if needed
|
||||||
String githubLogoutUrl = "https://github.com/logout";
|
String githubLogoutUrl = "https://github.com/logout";
|
||||||
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
logger.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||||
response.sendRedirect(githubLogoutUrl);
|
response.sendRedirect(githubLogoutUrl);
|
||||||
break;
|
break;
|
||||||
case "google":
|
case "google":
|
||||||
@@ -104,14 +106,13 @@ public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHand
|
|||||||
// String googleLogoutUrl =
|
// String googleLogoutUrl =
|
||||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||||
// + response.encodeRedirectURL(redirect_url);
|
// + response.encodeRedirectURL(redirect_url);
|
||||||
log.info("Google does not have a specific logout URL");
|
// logger.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||||
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
// response.sendRedirect(googleLogoutUrl);
|
||||||
// break;
|
// break;
|
||||||
default:
|
default:
|
||||||
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
String redirectUrl = request.getContextPath() + "/login?" + param;
|
||||||
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
logger.info("Redirecting to default logout URL: " + redirectUrl);
|
||||||
response.sendRedirect(defaultRedirectUrl);
|
response.sendRedirect(redirectUrl);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.oauth2;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
|
||||||
import stirling.software.SPDF.model.User;
|
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Slf4j
|
|
||||||
public class OAuth2Config {
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
|
||||||
|
|
||||||
// Client Registration Repository for OAUTH2 OIDC Login
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = "security.oauth2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
|
||||||
List<ClientRegistration> registrations = new ArrayList<>();
|
|
||||||
|
|
||||||
githubClientRegistration().ifPresent(registrations::add);
|
|
||||||
oidcClientRegistration().ifPresent(registrations::add);
|
|
||||||
googleClientRegistration().ifPresent(registrations::add);
|
|
||||||
keycloakClientRegistration().ifPresent(registrations::add);
|
|
||||||
|
|
||||||
if (registrations.isEmpty()) {
|
|
||||||
log.error("At least one OAuth2 provider must be configured");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new InMemoryClientRegistrationRepository(registrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> googleClientRegistration() {
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
GoogleProvider google = client.getGoogle();
|
|
||||||
return google != null && google.isSettingsValid()
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistration.withRegistrationId(google.getName())
|
|
||||||
.clientId(google.getClientId())
|
|
||||||
.clientSecret(google.getClientSecret())
|
|
||||||
.scope(google.getScopes())
|
|
||||||
.authorizationUri(google.getAuthorizationuri())
|
|
||||||
.tokenUri(google.getTokenuri())
|
|
||||||
.userInfoUri(google.getUserinfouri())
|
|
||||||
.userNameAttributeName(google.getUseAsUsername())
|
|
||||||
.clientName(google.getClientName())
|
|
||||||
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
|
||||||
.authorizationGrantType(
|
|
||||||
org.springframework.security.oauth2.core
|
|
||||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> keycloakClientRegistration() {
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
|
||||||
|
|
||||||
return keycloak != null && keycloak.isSettingsValid()
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
|
||||||
.registrationId(keycloak.getName())
|
|
||||||
.clientId(keycloak.getClientId())
|
|
||||||
.clientSecret(keycloak.getClientSecret())
|
|
||||||
.scope(keycloak.getScopes())
|
|
||||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
|
||||||
.clientName(keycloak.getClientName())
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
GithubProvider github = client.getGithub();
|
|
||||||
return github != null && github.isSettingsValid()
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistration.withRegistrationId(github.getName())
|
|
||||||
.clientId(github.getClientId())
|
|
||||||
.clientSecret(github.getClientSecret())
|
|
||||||
.scope(github.getScopes())
|
|
||||||
.authorizationUri(github.getAuthorizationuri())
|
|
||||||
.tokenUri(github.getTokenuri())
|
|
||||||
.userInfoUri(github.getUserinfouri())
|
|
||||||
.userNameAttributeName(github.getUseAsUsername())
|
|
||||||
.clientName(github.getClientName())
|
|
||||||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
|
||||||
.authorizationGrantType(
|
|
||||||
org.springframework.security.oauth2.core
|
|
||||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> oidcClientRegistration() {
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
|
|
||||||
if (oauth == null
|
|
||||||
|| oauth.getIssuer() == null
|
|
||||||
|| oauth.getIssuer().isEmpty()
|
|
||||||
|| oauth.getClientId() == null
|
|
||||||
|| oauth.getClientId().isEmpty()
|
|
||||||
|| oauth.getClientSecret() == null
|
|
||||||
|| oauth.getClientSecret().isEmpty()
|
|
||||||
|| oauth.getScopes() == null
|
|
||||||
|| oauth.getScopes().isEmpty()
|
|
||||||
|| oauth.getUseAsUsername() == null
|
|
||||||
|| oauth.getUseAsUsername().isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
return Optional.of(
|
|
||||||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
|
||||||
.registrationId("oidc")
|
|
||||||
.clientId(oauth.getClientId())
|
|
||||||
.clientSecret(oauth.getClientSecret())
|
|
||||||
.scope(oauth.getScopes())
|
|
||||||
.userNameAttributeName(oauth.getUseAsUsername())
|
|
||||||
.clientName("OIDC")
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
|
||||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
|
||||||
*/
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = "security.oauth2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
|
||||||
return (authorities) -> {
|
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
|
||||||
|
|
||||||
authorities.forEach(
|
|
||||||
authority -> {
|
|
||||||
// Add existing OAUTH2 Authorities
|
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
|
||||||
|
|
||||||
// Add Authorities from database for existing user, if user is present.
|
|
||||||
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
|
||||||
String useAsUsername =
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getOAUTH2()
|
|
||||||
.getUseAsUsername();
|
|
||||||
Optional<User> userOpt =
|
|
||||||
userService.findByUsernameIgnoreCase(
|
|
||||||
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
|
||||||
if (userOpt.isPresent()) {
|
|
||||||
User user = userOpt.get();
|
|
||||||
if (user != null) {
|
|
||||||
mappedAuthorities.add(
|
|
||||||
new SimpleGrantedAuthority(
|
|
||||||
userService.findRole(user).getAuthority()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return mappedAuthorities;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
|
||||||
import org.springframework.security.authentication.DisabledException;
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
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 CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationFailure(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
AuthenticationException exception)
|
|
||||||
throws IOException, ServletException {
|
|
||||||
|
|
||||||
if (exception instanceof BadCredentialsException) {
|
|
||||||
log.error("BadCredentialsException", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof DisabledException) {
|
|
||||||
log.error("User is deactivated: ", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof LockedException) {
|
|
||||||
log.error("Account locked: ", exception);
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (exception instanceof Saml2AuthenticationException) {
|
|
||||||
log.error("SAML2 Authentication error: ", exception);
|
|
||||||
getRedirectStrategy()
|
|
||||||
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
log.error("Unhandled authentication exception", exception);
|
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
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.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class CustomSAMLAuthenticationSuccessHandler
|
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
public CustomSAMLAuthenticationSuccessHandler(
|
|
||||||
LoginAttemptService loginAttemptService, UserService userService) {
|
|
||||||
this.loginAttemptService = loginAttemptService;
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSuccess(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws ServletException, IOException {
|
|
||||||
|
|
||||||
String userName = request.getParameter("username");
|
|
||||||
if (userService.isUserDisabled(userName)) {
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loginAttemptService.loginSucceeded(userName);
|
|
||||||
|
|
||||||
// Get the saved request
|
|
||||||
HttpSession session = request.getSession(false);
|
|
||||||
SavedRequest savedRequest =
|
|
||||||
(session != null)
|
|
||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (savedRequest != null
|
|
||||||
&& !RequestUriUtils.isStaticResource(
|
|
||||||
request.getContextPath(), savedRequest.getRedirectUrl())) {
|
|
||||||
// Redirect to the original destination
|
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
|
||||||
} else {
|
|
||||||
// Redirect to the root URL (considering context path)
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLogoutSuccess(
|
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
|
||||||
throws IOException, ServletException {
|
|
||||||
|
|
||||||
String redirectUrl = determineTargetUrl(request, response, authentication);
|
|
||||||
|
|
||||||
if (response.isCommitted()) {
|
|
||||||
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String determineTargetUrl(
|
|
||||||
HttpServletRequest request,
|
|
||||||
HttpServletResponse response,
|
|
||||||
Authentication authentication) {
|
|
||||||
// Default to the root URL
|
|
||||||
return "/";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.saml;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
|
||||||
import org.springframework.security.saml2.provider.service.metadata.OpenSamlMetadataResolver;
|
|
||||||
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.DefaultRelyingPartyRegistrationResolver;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Slf4j
|
|
||||||
public class SamlConfig {
|
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public OpenSaml4AuthenticationProvider openSaml4AuthenticationProvider() {
|
|
||||||
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
|
|
||||||
provider.setResponseAuthenticationConverter(
|
|
||||||
responseToken -> {
|
|
||||||
Saml2AuthenticationToken token = responseToken.getToken();
|
|
||||||
log.info("Received SAML response: {}", token.getSaml2Response());
|
|
||||||
// Your custom conversion logic here
|
|
||||||
// For now, we'll just return the token as is
|
|
||||||
return token;
|
|
||||||
});
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = "security.saml.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
|
|
||||||
RelyingPartyRegistration registration =
|
|
||||||
RelyingPartyRegistration.withRegistrationId(
|
|
||||||
applicationProperties.getSecurity().getSAML().getRegistrationId())
|
|
||||||
.entityId(applicationProperties.getSecurity().getSAML().getEntityId())
|
|
||||||
.assertionConsumerServiceLocation(
|
|
||||||
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
|
|
||||||
+ "/login/saml2/sso/stirling")
|
|
||||||
.singleLogoutServiceLocation(
|
|
||||||
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
|
|
||||||
+ "/logout/saml2/slo")
|
|
||||||
.singleLogoutServiceResponseLocation(
|
|
||||||
applicationProperties.getSecurity().getSAML().getSpBaseUrl()
|
|
||||||
+ "/logout/saml2/slo")
|
|
||||||
.signingX509Credentials(credentials -> credentials.add(signingCredential()))
|
|
||||||
.assertingPartyDetails(
|
|
||||||
party ->
|
|
||||||
party.entityId(
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getEntityId())
|
|
||||||
.singleSignOnServiceLocation(
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getIdpMetadataLocation())
|
|
||||||
.wantAuthnRequestsSigned(true)
|
|
||||||
.verificationX509Credentials(
|
|
||||||
c -> c.add(this.realmCertificate())))
|
|
||||||
.build();
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2X509Credential signingCredential() {
|
|
||||||
log.info("Starting to load signing credential");
|
|
||||||
try {
|
|
||||||
Resource storeResource =
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getKeystoreResource();
|
|
||||||
log.info("Keystore resource: {}", storeResource.getDescription());
|
|
||||||
|
|
||||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
|
||||||
try (InputStream is = storeResource.getInputStream()) {
|
|
||||||
keyStore.load(
|
|
||||||
is,
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getKeystorePassword()
|
|
||||||
.toCharArray());
|
|
||||||
log.info("Keystore loaded successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
String keyAlias =
|
|
||||||
applicationProperties.getSecurity().getSAML().getKeystore().getKeyAlias();
|
|
||||||
log.info("Attempting to retrieve private key with alias: {}", keyAlias);
|
|
||||||
|
|
||||||
PrivateKey privateKey =
|
|
||||||
(PrivateKey)
|
|
||||||
keyStore.getKey(
|
|
||||||
keyAlias,
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getKeyPassword()
|
|
||||||
.toCharArray());
|
|
||||||
|
|
||||||
if (privateKey == null) {
|
|
||||||
log.error("Private key not found for alias: {}", keyAlias);
|
|
||||||
throw new RuntimeException("Private key not found in keystore");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Private key retrieved successfully");
|
|
||||||
|
|
||||||
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(keyAlias);
|
|
||||||
|
|
||||||
if (certificate == null) {
|
|
||||||
log.info("Certificate not found for alias: {}", keyAlias);
|
|
||||||
throw new RuntimeException("Certificate not found in keystore");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Certificate retrieved successfully. Subject: {}",
|
|
||||||
certificate.getSubjectX500Principal());
|
|
||||||
|
|
||||||
log.info("Signing credential created successfully");
|
|
||||||
return Saml2X509Credential.signing(privateKey, certificate);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error loading signing credential", e);
|
|
||||||
throw new RuntimeException("Error loading signing credential", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Saml2X509Credential realmCertificate() {
|
|
||||||
log.info("Starting to load realm certificate");
|
|
||||||
try {
|
|
||||||
Resource storeResource =
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getKeystoreResource();
|
|
||||||
log.info("Keystore resource: {}", storeResource.getDescription());
|
|
||||||
|
|
||||||
KeyStore keyStore = KeyStore.getInstance("JKS");
|
|
||||||
try (InputStream is = storeResource.getInputStream()) {
|
|
||||||
keyStore.load(
|
|
||||||
is,
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getKeystorePassword()
|
|
||||||
.toCharArray());
|
|
||||||
log.info("Keystore loaded successfully");
|
|
||||||
}
|
|
||||||
|
|
||||||
String realmCertificateAlias =
|
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
|
||||||
.getSAML()
|
|
||||||
.getKeystore()
|
|
||||||
.getRealmCertificateAlias();
|
|
||||||
log.info(
|
|
||||||
"Attempting to retrieve realm certificate with alias: {}",
|
|
||||||
realmCertificateAlias);
|
|
||||||
|
|
||||||
X509Certificate certificate =
|
|
||||||
(X509Certificate) keyStore.getCertificate(realmCertificateAlias);
|
|
||||||
|
|
||||||
if (certificate == null) {
|
|
||||||
log.error("Realm certificate not found for alias: {}", realmCertificateAlias);
|
|
||||||
throw new RuntimeException("Realm certificate not found in keystore");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Realm certificate retrieved successfully. Subject: {}",
|
|
||||||
certificate.getSubjectX500Principal());
|
|
||||||
|
|
||||||
log.info("Realm certificate credential created successfully");
|
|
||||||
return Saml2X509Credential.verification(certificate);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error loading realm certificate", e);
|
|
||||||
throw new RuntimeException("Error loading realm certificate", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
value = "security.saml.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public Saml2MetadataFilter metadataFilter(RelyingPartyRegistrationRepository registrations) {
|
|
||||||
DefaultRelyingPartyRegistrationResolver registrationResolver =
|
|
||||||
new DefaultRelyingPartyRegistrationResolver(registrations);
|
|
||||||
OpenSamlMetadataResolver metadataResolver = new OpenSamlMetadataResolver();
|
|
||||||
return new Saml2MetadataFilter(registrationResolver, metadataResolver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
|
||||||
public class CustomHttpSessionListener implements HttpSessionListener {
|
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sessionCreated(HttpSessionEvent se) {
|
|
||||||
log.info("Session created: " + se.getSession().getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sessionDestroyed(HttpSessionEvent se) {
|
|
||||||
log.info("Session destroyed: " + se.getSession().getId());
|
|
||||||
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SessionPersistentRegistry implements SessionRegistry {
|
|
||||||
|
|
||||||
private final SessionRepository sessionRepository;
|
|
||||||
|
|
||||||
@Value("${server.servlet.session.timeout:30m}")
|
|
||||||
private Duration defaultMaxInactiveInterval;
|
|
||||||
|
|
||||||
public SessionPersistentRegistry(SessionRepository sessionRepository) {
|
|
||||||
this.sessionRepository = sessionRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Object> getAllPrincipals() {
|
|
||||||
List<SessionEntity> sessions = sessionRepository.findAll();
|
|
||||||
List<Object> principals = new ArrayList<>();
|
|
||||||
for (SessionEntity session : sessions) {
|
|
||||||
principals.add(session.getPrincipalName());
|
|
||||||
}
|
|
||||||
return principals;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<SessionInformation> getAllSessions(
|
|
||||||
Object principal, boolean includeExpiredSessions) {
|
|
||||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
|
||||||
String principalName = null;
|
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
principalName = ((UserDetails) principal).getUsername();
|
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
principalName = ((OAuth2User) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
|
||||||
principalName = (String) principal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (principalName != null) {
|
|
||||||
List<SessionEntity> sessionEntities =
|
|
||||||
sessionRepository.findByPrincipalName(principalName);
|
|
||||||
for (SessionEntity sessionEntity : sessionEntities) {
|
|
||||||
if (includeExpiredSessions || !sessionEntity.isExpired()) {
|
|
||||||
sessionInformations.add(
|
|
||||||
new SessionInformation(
|
|
||||||
sessionEntity.getPrincipalName(),
|
|
||||||
sessionEntity.getSessionId(),
|
|
||||||
sessionEntity.getLastRequest()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sessionInformations;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void registerNewSession(String sessionId, Object principal) {
|
|
||||||
String principalName = null;
|
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
principalName = ((UserDetails) principal).getUsername();
|
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
principalName = ((OAuth2User) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
|
||||||
principalName = (String) principal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (principalName != null) {
|
|
||||||
SessionEntity sessionEntity = new SessionEntity();
|
|
||||||
sessionEntity.setSessionId(sessionId);
|
|
||||||
sessionEntity.setPrincipalName(principalName);
|
|
||||||
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
|
||||||
sessionEntity.setExpired(false);
|
|
||||||
sessionRepository.save(sessionEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void removeSessionInformation(String sessionId) {
|
|
||||||
sessionRepository.deleteById(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional
|
|
||||||
public void refreshLastRequest(String sessionId) {
|
|
||||||
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
|
||||||
if (sessionEntityOpt.isPresent()) {
|
|
||||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
|
||||||
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
|
||||||
sessionRepository.save(sessionEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SessionInformation getSessionInformation(String sessionId) {
|
|
||||||
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
|
||||||
if (sessionEntityOpt.isPresent()) {
|
|
||||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
|
||||||
return new SessionInformation(
|
|
||||||
sessionEntity.getPrincipalName(),
|
|
||||||
sessionEntity.getSessionId(),
|
|
||||||
sessionEntity.getLastRequest());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve all non-expired sessions
|
|
||||||
public List<SessionEntity> getAllSessionsNotExpired() {
|
|
||||||
return sessionRepository.findByExpired(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve all sessions
|
|
||||||
public List<SessionEntity> getAllSessions() {
|
|
||||||
return sessionRepository.findAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark a session as expired
|
|
||||||
public void expireSession(String sessionId) {
|
|
||||||
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
|
||||||
if (sessionEntityOpt.isPresent()) {
|
|
||||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
|
||||||
sessionEntity.setExpired(true); // Set expired to true
|
|
||||||
sessionRepository.save(sessionEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the maximum inactive interval for sessions
|
|
||||||
public int getMaxInactiveInterval() {
|
|
||||||
return (int) defaultMaxInactiveInterval.getSeconds();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve a session entity by session ID
|
|
||||||
public SessionEntity getSessionEntity(String sessionId) {
|
|
||||||
return sessionRepository.findBySessionId(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update session details by principal name
|
|
||||||
public void updateSessionByPrincipalName(
|
|
||||||
String principalName, boolean expired, Date lastRequest) {
|
|
||||||
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the latest session for a given principal name
|
|
||||||
public Optional<SessionEntity> findLatestSession(String principalName) {
|
|
||||||
List<SessionEntity> allSessions = sessionRepository.findByPrincipalName(principalName);
|
|
||||||
if (allSessions.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort sessions by lastRequest in descending order
|
|
||||||
Collections.sort(
|
|
||||||
allSessions,
|
|
||||||
new Comparator<SessionEntity>() {
|
|
||||||
@Override
|
|
||||||
public int compare(SessionEntity s1, SessionEntity s2) {
|
|
||||||
// Sort by lastRequest in descending order
|
|
||||||
return s2.getLastRequest().compareTo(s1.getLastRequest());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The first session in the list is the latest session for the given principal name
|
|
||||||
return Optional.of(allSessions.get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class SessionRegistryConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SessionRegistryImpl sessionRegistry() {
|
|
||||||
return new SessionRegistryImpl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public SessionPersistentRegistry sessionPersistentRegistry(
|
|
||||||
SessionRepository sessionRepository) {
|
|
||||||
return new SessionPersistentRegistry(sessionRepository);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.data.jpa.repository.Modifying;
|
|
||||||
import org.springframework.data.jpa.repository.Query;
|
|
||||||
import org.springframework.data.repository.query.Param;
|
|
||||||
import org.springframework.stereotype.Repository;
|
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
public interface SessionRepository extends JpaRepository<SessionEntity, String> {
|
|
||||||
List<SessionEntity> findByPrincipalName(String principalName);
|
|
||||||
|
|
||||||
List<SessionEntity> findByExpired(boolean expired);
|
|
||||||
|
|
||||||
SessionEntity findBySessionId(String sessionId);
|
|
||||||
|
|
||||||
@Modifying
|
|
||||||
@Transactional
|
|
||||||
@Query(
|
|
||||||
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
|
|
||||||
void saveByPrincipalName(
|
|
||||||
@Param("expired") boolean expired,
|
|
||||||
@Param("lastRequest") Date lastRequest,
|
|
||||||
@Param("principalName") String principalName);
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
@Component
|
|
||||||
public class SessionScheduled {
|
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
|
||||||
|
|
||||||
@Scheduled(cron = "0 0/5 * * * ?")
|
|
||||||
public void expireSessions() {
|
|
||||||
Instant now = Instant.now();
|
|
||||||
|
|
||||||
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
|
||||||
List<SessionInformation> sessionInformations =
|
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
|
||||||
for (SessionInformation sessionInformation : sessionInformations) {
|
|
||||||
Date lastRequest = sessionInformation.getLastRequest();
|
|
||||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
|
||||||
Instant expirationTime =
|
|
||||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
|
||||||
if (now.isAfter(expirationTime)) {
|
|
||||||
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
|
||||||
import stirling.software.SPDF.service.PdfImageRemovalService;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
|
|
||||||
* from a PDF file to reduce its size.
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/v1/general")
|
|
||||||
public class PdfImageRemovalController {
|
|
||||||
|
|
||||||
// Service for removing images from PDFs
|
|
||||||
@Autowired private PdfImageRemovalService pdfImageRemovalService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for dependency injection of PdfImageRemovalService.
|
|
||||||
*
|
|
||||||
* @param pdfImageRemovalService The service used for removing images from PDFs.
|
|
||||||
*/
|
|
||||||
public PdfImageRemovalController(PdfImageRemovalService pdfImageRemovalService) {
|
|
||||||
this.pdfImageRemovalService = pdfImageRemovalService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Endpoint to remove images from a PDF file.
|
|
||||||
*
|
|
||||||
* <p>This method processes the uploaded PDF file, removes all images, and returns the modified
|
|
||||||
* PDF file with a new name indicating that images were removed.
|
|
||||||
*
|
|
||||||
* @param file The PDF file with images to be removed.
|
|
||||||
* @return ResponseEntity containing the modified PDF file as byte array with appropriate
|
|
||||||
* content type and filename.
|
|
||||||
* @throws IOException If an error occurs while processing the PDF file.
|
|
||||||
*/
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-image-pdf")
|
|
||||||
@Operation(
|
|
||||||
summary = "Remove images from file to reduce the file size.",
|
|
||||||
description =
|
|
||||||
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
|
|
||||||
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
|
|
||||||
|
|
||||||
MultipartFile pdf = file.getFileInput();
|
|
||||||
|
|
||||||
// Convert the MultipartFile to a byte array
|
|
||||||
byte[] pdfBytes = pdf.getBytes();
|
|
||||||
|
|
||||||
// Load the PDF document from the byte array
|
|
||||||
PDDocument document = Loader.loadPDF(pdfBytes);
|
|
||||||
|
|
||||||
// Remove images from the PDF document using the service
|
|
||||||
PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
|
|
||||||
|
|
||||||
// Create a ByteArrayOutputStream to hold the modified PDF data
|
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
||||||
|
|
||||||
// Save the modified PDF document to the output stream
|
|
||||||
modifiedDocument.save(outputStream);
|
|
||||||
modifiedDocument.close();
|
|
||||||
|
|
||||||
// Generate a new filename for the modified PDF
|
|
||||||
String mergedFileName =
|
|
||||||
pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_images.pdf";
|
|
||||||
|
|
||||||
// Convert the byte array to a web response and return it
|
|
||||||
return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -13,8 +12,8 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
|
import org.springframework.security.core.session.SessionRegistry;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@@ -31,8 +30,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
@@ -44,8 +41,6 @@ public class UserController {
|
|||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
|
|
||||||
@Autowired SessionPersistentRegistry sessionRegistry;
|
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
||||||
@@ -208,10 +203,9 @@ public class UserController {
|
|||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam(name = "password", required = false) String password,
|
@RequestParam(name = "password") String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
@RequestParam(name = "authType") String authType,
|
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
boolean forceChange)
|
boolean forceChange)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
@@ -243,15 +237,7 @@ public class UserController {
|
|||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
userService.saveUser(username, password, role, forceChange);
|
||||||
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
|
||||||
} else {
|
|
||||||
if (password.isBlank()) {
|
|
||||||
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
|
||||||
}
|
|
||||||
userService.saveUser(username, password, role, forceChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
@@ -261,8 +247,7 @@ public class UserController {
|
|||||||
public RedirectView changeRole(
|
public RedirectView changeRole(
|
||||||
@RequestParam(name = "username") String username,
|
@RequestParam(name = "username") String username,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
Authentication authentication)
|
Authentication authentication) {
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
|
|
||||||
@@ -293,60 +278,6 @@ public class UserController {
|
|||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
|
|
||||||
userService.changeRole(user, role);
|
userService.changeRole(user, role);
|
||||||
|
|
||||||
return new RedirectView(
|
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
|
||||||
@PostMapping("/admin/changeUserEnabled/{username}")
|
|
||||||
public RedirectView changeUserEnabled(
|
|
||||||
@PathVariable("username") String username,
|
|
||||||
@RequestParam("enabled") boolean enabled,
|
|
||||||
Authentication authentication)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
|
||||||
|
|
||||||
if (!userOpt.isPresent()) {
|
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
|
||||||
}
|
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
|
||||||
}
|
|
||||||
// Get the currently authenticated username
|
|
||||||
String currentUsername = authentication.getName();
|
|
||||||
|
|
||||||
// Check if the provided username matches the current session's username
|
|
||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
|
||||||
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
|
|
||||||
}
|
|
||||||
User user = userOpt.get();
|
|
||||||
|
|
||||||
userService.changeUserEnabled(user, enabled);
|
|
||||||
|
|
||||||
if (!enabled) {
|
|
||||||
// Invalidate all sessions if the user is being disabled
|
|
||||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
|
||||||
String userNameP = "";
|
|
||||||
for (Object principal : principals) {
|
|
||||||
List<SessionInformation> sessionsInformations =
|
|
||||||
sessionRegistry.getAllSessions(principal, false);
|
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
userNameP = ((UserDetails) principal).getUsername();
|
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
userNameP = ((OAuth2User) principal).getName();
|
|
||||||
} else if (principal instanceof String) {
|
|
||||||
userNameP = (String) principal;
|
|
||||||
}
|
|
||||||
if (userNameP.equalsIgnoreCase(username)) {
|
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RedirectView(
|
return new RedirectView(
|
||||||
"/addUsers", true); // Redirect to account page after adding the user
|
"/addUsers", true); // Redirect to account page after adding the user
|
||||||
}
|
}
|
||||||
@@ -354,7 +285,7 @@ public class UserController {
|
|||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@PostMapping("/admin/deleteUser/{username}")
|
@PostMapping("/admin/deleteUser/{username}")
|
||||||
public RedirectView deleteUser(
|
public RedirectView deleteUser(
|
||||||
@PathVariable("username") String username, Authentication authentication) {
|
@PathVariable(name = "username") String username, Authentication authentication) {
|
||||||
|
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
|
||||||
@@ -367,18 +298,27 @@ public class UserController {
|
|||||||
if (currentUsername.equalsIgnoreCase(username)) {
|
if (currentUsername.equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
|
||||||
}
|
}
|
||||||
|
invalidateUserSessions(username);
|
||||||
// Invalidate all sessions before deleting the user
|
|
||||||
List<SessionInformation> sessionsInformations =
|
|
||||||
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
|
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
|
||||||
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
|
|
||||||
}
|
|
||||||
userService.deleteUser(username);
|
userService.deleteUser(username);
|
||||||
return new RedirectView("/addUsers", true);
|
return new RedirectView("/addUsers", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired private SessionRegistry sessionRegistry;
|
||||||
|
|
||||||
|
private void invalidateUserSessions(String username) {
|
||||||
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
|
if (principal instanceof UserDetails) {
|
||||||
|
UserDetails userDetails = (UserDetails) principal;
|
||||||
|
if (userDetails.getUsername().equals(username)) {
|
||||||
|
for (SessionInformation session :
|
||||||
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
|
session.expireNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/get-api-key")
|
@PostMapping("/get-api-key")
|
||||||
public ResponseEntity<String> getApiKey(Principal principal) {
|
public ResponseEntity<String> getApiKey(Principal principal) {
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -30,10 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -73,87 +60,15 @@ public class ConvertImgPDFController {
|
|||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
pdfBytes,
|
||||||
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
|
imageFormat.toUpperCase(),
|
||||||
colorTypeResult,
|
colorTypeResult,
|
||||||
singleImage,
|
singleImage,
|
||||||
Integer.valueOf(dpi),
|
Integer.valueOf(dpi),
|
||||||
filename);
|
filename);
|
||||||
|
|
||||||
if (result == null || result.length == 0) {
|
if (result == null || result.length == 0) {
|
||||||
logger.error("resultant bytes for {} is null, error converting ", filename);
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
}
|
}
|
||||||
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
|
|
||||||
throw new IOException("Python is not installed. Required for WebP conversion.");
|
|
||||||
} else if (imageFormat.equalsIgnoreCase("webp")
|
|
||||||
&& CheckProgramInstall.isPythonAvailable()) {
|
|
||||||
// Write the output stream to a temp file
|
|
||||||
Path tempFile = Files.createTempFile("temp_png", ".png");
|
|
||||||
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
|
||||||
fos.write(result);
|
|
||||||
fos.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add(pythonVersion);
|
|
||||||
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
|
||||||
|
|
||||||
// Create a temporary directory for the output WebP files
|
|
||||||
Path tempOutputDir = Files.createTempDirectory("webp_output");
|
|
||||||
if (singleImage) {
|
|
||||||
// Run the Python script to convert PNG to WebP
|
|
||||||
command.add(tempFile.toString());
|
|
||||||
command.add(tempOutputDir.toString());
|
|
||||||
command.add("--single");
|
|
||||||
} else {
|
|
||||||
// Save the uploaded PDF to a temporary file
|
|
||||||
Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
|
|
||||||
file.transferTo(tempPdfPath.toFile());
|
|
||||||
// Run the Python script to convert PDF to WebP
|
|
||||||
command.add(tempPdfPath.toString());
|
|
||||||
command.add(tempOutputDir.toString());
|
|
||||||
}
|
|
||||||
command.add("--dpi");
|
|
||||||
command.add(dpi);
|
|
||||||
ProcessExecutorResult resultProcess =
|
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
|
||||||
.runCommandWithOutputHandling(command);
|
|
||||||
|
|
||||||
// Find all WebP files in the output directory
|
|
||||||
List<Path> webpFiles =
|
|
||||||
Files.walk(tempOutputDir)
|
|
||||||
.filter(path -> path.toString().endsWith(".webp"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (webpFiles.isEmpty()) {
|
|
||||||
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
|
|
||||||
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] bodyBytes = new byte[0];
|
|
||||||
|
|
||||||
if (webpFiles.size() == 1) {
|
|
||||||
// Return the single WebP file directly
|
|
||||||
Path webpFilePath = webpFiles.get(0);
|
|
||||||
bodyBytes = Files.readAllBytes(webpFilePath);
|
|
||||||
} else {
|
|
||||||
// Create a ZIP file containing all WebP images
|
|
||||||
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
|
|
||||||
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
|
|
||||||
for (Path webpFile : webpFiles) {
|
|
||||||
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
|
|
||||||
Files.copy(webpFile, zos);
|
|
||||||
zos.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bodyBytes = zipOutputStream.toByteArray();
|
|
||||||
}
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
|
||||||
result = bodyBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (singleImage) {
|
if (singleImage) {
|
||||||
String docName = filename + "." + imageFormat;
|
String docName = filename + "." + imageFormat;
|
||||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ public class ConvertWebsiteToPDF {
|
|||||||
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
|
||||||
throw new IllegalArgumentException("Invalid URL format provided.");
|
throw new IllegalArgumentException("Invalid URL format provided.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate the URL is reachable
|
|
||||||
if (!GeneralUtils.isURLReachable(URL)) {
|
|
||||||
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class AutoRenameController {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
private static final Logger logger = LoggerFactory.getLogger(AutoRenameController.class);
|
||||||
|
|
||||||
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
|
||||||
private static final int LINE_LIMIT = 200;
|
private static final int LINE_LIMIT = 11;
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -17,7 +17,6 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -51,31 +50,31 @@ public class BlankPageController {
|
|||||||
int threshold = request.getThreshold();
|
int threshold = request.getThreshold();
|
||||||
float whitePercent = request.getWhitePercent();
|
float whitePercent = request.getWhitePercent();
|
||||||
|
|
||||||
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
|
PDDocument document = null;
|
||||||
|
try {
|
||||||
|
document = Loader.loadPDF(inputFile.getBytes());
|
||||||
PDPageTree pages = document.getDocumentCatalog().getPages();
|
PDPageTree pages = document.getDocumentCatalog().getPages();
|
||||||
PDFTextStripper textStripper = new PDFTextStripper();
|
PDFTextStripper textStripper = new PDFTextStripper();
|
||||||
|
|
||||||
List<PDPage> nonBlankPages = new ArrayList<>();
|
List<Integer> pagesToKeepIndex = new ArrayList<>();
|
||||||
List<PDPage> blankPages = new ArrayList<>();
|
|
||||||
int pageIndex = 0;
|
int pageIndex = 0;
|
||||||
|
|
||||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
pdfRenderer.setSubsamplingAllowed(true);
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
for (PDPage page : pages) {
|
for (PDPage page : pages) {
|
||||||
logger.info("checking page {}", pageIndex);
|
logger.info("checking page " + pageIndex);
|
||||||
textStripper.setStartPage(pageIndex + 1);
|
textStripper.setStartPage(pageIndex + 1);
|
||||||
textStripper.setEndPage(pageIndex + 1);
|
textStripper.setEndPage(pageIndex + 1);
|
||||||
String pageText = textStripper.getText(document);
|
String pageText = textStripper.getText(document);
|
||||||
boolean hasText = !pageText.trim().isEmpty();
|
boolean hasText = !pageText.trim().isEmpty();
|
||||||
|
|
||||||
boolean blank = true;
|
Boolean blank = true;
|
||||||
if (hasText) {
|
if (hasText) {
|
||||||
logger.info("page {} has text, not blank", pageIndex);
|
logger.info("page " + pageIndex + " has text, not blank");
|
||||||
blank = false;
|
blank = false;
|
||||||
} else {
|
} else {
|
||||||
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
boolean hasImages = PdfUtils.hasImagesOnPage(page);
|
||||||
if (hasImages) {
|
if (hasImages) {
|
||||||
logger.info("page {} has image, running blank detection", pageIndex);
|
logger.info("page " + pageIndex + " has image, running blank detection");
|
||||||
// Render image and save as temp file
|
// Render image and save as temp file
|
||||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30);
|
||||||
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
blank = isBlankImage(image, threshold, whitePercent, threshold);
|
||||||
@@ -83,57 +82,34 @@ public class BlankPageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (blank) {
|
if (blank) {
|
||||||
logger.info("Skipping, Image was blank for page #{}", pageIndex);
|
logger.info("Skipping, Image was blank for page #" + pageIndex);
|
||||||
blankPages.add(page);
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("page {} has image which is not blank", pageIndex);
|
logger.info("page " + pageIndex + " has image which is not blank");
|
||||||
nonBlankPages.add(page);
|
pagesToKeepIndex.add(pageIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageIndex++;
|
pageIndex++;
|
||||||
}
|
}
|
||||||
|
// Remove pages not present in pagesToKeepIndex
|
||||||
|
List<Integer> pageIndices =
|
||||||
|
IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList());
|
||||||
|
Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal
|
||||||
|
for (Integer i : pageIndices) {
|
||||||
|
if (!pagesToKeepIndex.contains(i)) {
|
||||||
|
pages.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
ZipOutputStream zos = new ZipOutputStream(baos);
|
document,
|
||||||
|
|
||||||
String filename =
|
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_blanksRemoved.pdf");
|
||||||
if (!nonBlankPages.isEmpty()) {
|
|
||||||
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
|
|
||||||
} else {
|
|
||||||
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
|
|
||||||
createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
|
|
||||||
}
|
|
||||||
|
|
||||||
zos.close();
|
|
||||||
|
|
||||||
logger.info("Returning ZIP file: {}", filename + "_processed.zip");
|
|
||||||
return WebResponseUtils.boasToWebResponse(
|
|
||||||
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("exception", e);
|
logger.error("exception", e);
|
||||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
} finally {
|
||||||
}
|
if (document != null) document.close();
|
||||||
|
|
||||||
public void createZipEntry(ZipOutputStream zos, List<PDPage> pages, String entryName)
|
|
||||||
throws IOException {
|
|
||||||
try (PDDocument document = new PDDocument()) {
|
|
||||||
|
|
||||||
for (PDPage page : pages) {
|
|
||||||
document.addPage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
ZipEntry zipEntry = new ZipEntry(entryName);
|
|
||||||
zos.putNextEntry(zipEntry);
|
|
||||||
document.save(zos);
|
|
||||||
zos.closeEntry();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
|
||||||
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;
|
||||||
@@ -77,11 +76,6 @@ public class ExtractImageScansController {
|
|||||||
Path tempZipFile = null;
|
Path tempZipFile = null;
|
||||||
List<Path> tempDirs = new ArrayList<>();
|
List<Path> tempDirs = new ArrayList<>();
|
||||||
|
|
||||||
if (!CheckProgramInstall.isPythonAvailable()) {
|
|
||||||
throw new IOException("Python is not installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
|
||||||
try {
|
try {
|
||||||
// Check if input file is a PDF
|
// Check if input file is a PDF
|
||||||
if ("pdf".equalsIgnoreCase(extension)) {
|
if ("pdf".equalsIgnoreCase(extension)) {
|
||||||
@@ -123,7 +117,7 @@ public class ExtractImageScansController {
|
|||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
pythonVersion,
|
"python3",
|
||||||
"./scripts/split_photos.py",
|
"./scripts/split_photos.py",
|
||||||
images.get(i),
|
images.get(i),
|
||||||
tempDir.toString(),
|
tempDir.toString(),
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.Image;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.awt.image.RenderedImage;
|
import java.awt.image.RenderedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.zip.Deflater;
|
import java.util.zip.Deflater;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
@@ -39,8 +33,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
import stirling.software.SPDF.model.api.PDFWithImageFormatRequest;
|
||||||
import stirling.software.SPDF.utils.ImageProcessingUtils;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -54,18 +47,15 @@ public class ExtractImagesController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Extract images from a PDF file",
|
summary = "Extract images from a PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input: PDF Output: IMAGE/ZIP Type: SIMO")
|
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
|
||||||
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request)
|
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFWithImageFormatRequest request)
|
||||||
throws IOException, InterruptedException, ExecutionException {
|
throws IOException {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String format = request.getFormat();
|
||||||
boolean allowDuplicates = request.isAllowDuplicates();
|
|
||||||
System.out.println(
|
|
||||||
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
|
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
|
||||||
|
|
||||||
// Determine if multithreading should be used based on PDF size or number of pages
|
System.out.println(
|
||||||
boolean useMultithreading = shouldUseMultithreading(file, document);
|
System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format);
|
||||||
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
// Create ByteArrayOutputStream to write zip file to byte array
|
// Create ByteArrayOutputStream to write zip file to byte array
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
@@ -76,58 +66,71 @@ public class ExtractImagesController {
|
|||||||
// Set compression level
|
// Set compression level
|
||||||
zos.setLevel(Deflater.BEST_COMPRESSION);
|
zos.setLevel(Deflater.BEST_COMPRESSION);
|
||||||
|
|
||||||
|
int imageIndex = 1;
|
||||||
String filename =
|
String filename =
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "");
|
.replaceFirst("[.][^.]+$", "");
|
||||||
Set<byte[]> processedImages = new HashSet<>();
|
int pageNum = 0;
|
||||||
|
Set<Integer> processedImages = new HashSet<>();
|
||||||
|
// Iterate over each page
|
||||||
|
for (PDPage page : document.getPages()) {
|
||||||
|
++pageNum;
|
||||||
|
// Extract images from page
|
||||||
|
for (COSName name : page.getResources().getXObjectNames()) {
|
||||||
|
if (page.getResources().isImageXObject(name)) {
|
||||||
|
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
||||||
|
int imageHash = image.hashCode();
|
||||||
|
if (processedImages.contains(imageHash)) {
|
||||||
|
continue; // Skip already processed images
|
||||||
|
}
|
||||||
|
processedImages.add(imageHash);
|
||||||
|
|
||||||
if (useMultithreading) {
|
// Convert image to desired format
|
||||||
// Executor service to handle multithreading
|
RenderedImage renderedImage = image.getImage();
|
||||||
ExecutorService executor =
|
BufferedImage bufferedImage = null;
|
||||||
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
|
if ("png".equalsIgnoreCase(format)) {
|
||||||
Set<Future<Void>> futures = new HashSet<>();
|
bufferedImage =
|
||||||
|
new BufferedImage(
|
||||||
|
renderedImage.getWidth(),
|
||||||
|
renderedImage.getHeight(),
|
||||||
|
BufferedImage.TYPE_INT_ARGB);
|
||||||
|
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
||||||
|
bufferedImage =
|
||||||
|
new BufferedImage(
|
||||||
|
renderedImage.getWidth(),
|
||||||
|
renderedImage.getHeight(),
|
||||||
|
BufferedImage.TYPE_INT_RGB);
|
||||||
|
} else if ("gif".equalsIgnoreCase(format)) {
|
||||||
|
bufferedImage =
|
||||||
|
new BufferedImage(
|
||||||
|
renderedImage.getWidth(),
|
||||||
|
renderedImage.getHeight(),
|
||||||
|
BufferedImage.TYPE_BYTE_INDEXED);
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate over each page
|
// Write image to zip file
|
||||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
String imageName =
|
||||||
PDPage page = document.getPage(pgNum);
|
filename + "_" + imageIndex + " (Page " + pageNum + ")." + format;
|
||||||
int pageNum = document.getPages().indexOf(page) + 1;
|
ZipEntry zipEntry = new ZipEntry(imageName);
|
||||||
// Submit a task for processing each page
|
zos.putNextEntry(zipEntry);
|
||||||
Future<Void> future =
|
|
||||||
executor.submit(
|
|
||||||
() -> {
|
|
||||||
extractImagesFromPage(
|
|
||||||
page,
|
|
||||||
format,
|
|
||||||
filename,
|
|
||||||
pageNum,
|
|
||||||
processedImages,
|
|
||||||
zos,
|
|
||||||
allowDuplicates);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
futures.add(future);
|
Graphics2D g = bufferedImage.createGraphics();
|
||||||
}
|
g.drawImage((Image) renderedImage, 0, 0, null);
|
||||||
|
g.dispose();
|
||||||
|
// Write image bytes to zip file
|
||||||
|
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(bufferedImage, format, imageBaos);
|
||||||
|
zos.write(imageBaos.toByteArray());
|
||||||
|
|
||||||
// Wait for all tasks to complete
|
zos.closeEntry();
|
||||||
for (Future<Void> future : futures) {
|
imageIndex++;
|
||||||
future.get();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Close executor service
|
|
||||||
executor.shutdown();
|
|
||||||
} else {
|
|
||||||
// Single-threaded extraction
|
|
||||||
for (int pgNum = 0; pgNum < document.getPages().getCount(); pgNum++) {
|
|
||||||
PDPage page = document.getPage(pgNum);
|
|
||||||
extractImagesFromPage(
|
|
||||||
page, format, filename, pgNum + 1, processedImages, zos, allowDuplicates);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close PDDocument and ZipOutputStream
|
// Close ZipOutputStream and PDDocument
|
||||||
document.close();
|
|
||||||
zos.close();
|
zos.close();
|
||||||
|
document.close();
|
||||||
|
|
||||||
// Create ByteArrayResource from byte array
|
// Create ByteArrayResource from byte array
|
||||||
byte[] zipContents = baos.toByteArray();
|
byte[] zipContents = baos.toByteArray();
|
||||||
@@ -135,85 +138,4 @@ public class ExtractImagesController {
|
|||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.boasToWebResponse(
|
||||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldUseMultithreading(MultipartFile file, PDDocument document) {
|
|
||||||
// Criteria: Use multithreading if file size > 10MB or number of pages > 20
|
|
||||||
long fileSizeInMB = file.getSize() / (1024 * 1024);
|
|
||||||
int numberOfPages = document.getPages().getCount();
|
|
||||||
return fileSizeInMB > 10 || numberOfPages > 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extractImagesFromPage(
|
|
||||||
PDPage page,
|
|
||||||
String format,
|
|
||||||
String filename,
|
|
||||||
int pageNum,
|
|
||||||
Set<byte[]> processedImages,
|
|
||||||
ZipOutputStream zos,
|
|
||||||
boolean allowDuplicates)
|
|
||||||
throws IOException {
|
|
||||||
MessageDigest md;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("MD5");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
logger.error("MD5 algorithm not available for extractImages hash.", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (page.getResources() == null || page.getResources().getXObjectNames() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int count = 1;
|
|
||||||
for (COSName name : page.getResources().getXObjectNames()) {
|
|
||||||
if (page.getResources().isImageXObject(name)) {
|
|
||||||
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
|
|
||||||
if (!allowDuplicates) {
|
|
||||||
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
|
|
||||||
byte[] imageHash = md.digest(data);
|
|
||||||
synchronized (processedImages) {
|
|
||||||
if (processedImages.stream()
|
|
||||||
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
|
|
||||||
continue; // Skip already processed images
|
|
||||||
}
|
|
||||||
processedImages.add(imageHash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderedImage renderedImage = image.getImage();
|
|
||||||
|
|
||||||
// Convert to standard RGB colorspace if needed
|
|
||||||
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
|
|
||||||
|
|
||||||
// Write image to zip file
|
|
||||||
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
|
|
||||||
synchronized (zos) {
|
|
||||||
zos.putNextEntry(new ZipEntry(imageName));
|
|
||||||
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
|
|
||||||
ImageIO.write(bufferedImage, format, imageBaos);
|
|
||||||
zos.write(imageBaos.toByteArray());
|
|
||||||
zos.closeEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private BufferedImage convertToRGB(RenderedImage renderedImage, String format) {
|
|
||||||
int width = renderedImage.getWidth();
|
|
||||||
int height = renderedImage.getHeight();
|
|
||||||
BufferedImage rgbImage;
|
|
||||||
|
|
||||||
if ("png".equalsIgnoreCase(format)) {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
|
||||||
} else if ("jpeg".equalsIgnoreCase(format) || "jpg".equalsIgnoreCase(format)) {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
} else if ("gif".equalsIgnoreCase(format)) {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED);
|
|
||||||
} else {
|
|
||||||
rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
|
||||||
}
|
|
||||||
|
|
||||||
Graphics2D g = rgbImage.createGraphics();
|
|
||||||
g.drawImage((Image) renderedImage, 0, 0, null);
|
|
||||||
g.dispose();
|
|
||||||
return rgbImage;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import java.util.stream.Collectors;
|
|||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
@@ -26,7 +27,6 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
@@ -37,10 +37,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OCRController {
|
public class OCRController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
private static final Logger logger = LoggerFactory.getLogger(OCRController.class);
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
String tessdataDir = "/usr/share/tessdata";
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ public class ApiDocService {
|
|||||||
|
|
||||||
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
Map<String, List<String>> outputToFileTypes = new HashMap<>();
|
||||||
|
|
||||||
public List<String> getExtensionTypes(boolean output, String operationName) {
|
public List getExtensionTypes(boolean output, String operationName) {
|
||||||
if (outputToFileTypes.size() == 0) {
|
if (outputToFileTypes.size() == 0) {
|
||||||
outputToFileTypes.put("PDF", Arrays.asList("pdf"));
|
outputToFileTypes.put("PDF", Arrays.asList("pdf"));
|
||||||
outputToFileTypes.put(
|
outputToFileTypes.put(
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
package stirling.software.SPDF.controller.api.security;
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||||
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -25,7 +32,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import stirling.software.SPDF.model.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
import stirling.software.SPDF.pdf.TextFinder;
|
import stirling.software.SPDF.pdf.TextFinder;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -75,9 +81,22 @@ public class RedactController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (convertPDFToImage) {
|
if (convertPDFToImage) {
|
||||||
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
PDDocument imageDocument = new PDDocument();
|
||||||
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
pdfRenderer.setSubsamplingAllowed(true);
|
||||||
|
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||||
|
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
|
||||||
|
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
|
||||||
|
imageDocument.addPage(newPage);
|
||||||
|
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
imageDocument, newPage, AppendMode.APPEND, true, true);
|
||||||
|
contentStream.drawImage(pdImage, 0, 0);
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
document.close();
|
document.close();
|
||||||
document = convertedPdf;
|
document = imageDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -61,7 +60,6 @@ public class WatermarkController {
|
|||||||
float opacity = request.getOpacity();
|
float opacity = request.getOpacity();
|
||||||
int widthSpacer = request.getWidthSpacer();
|
int widthSpacer = request.getWidthSpacer();
|
||||||
int heightSpacer = request.getHeightSpacer();
|
int heightSpacer = request.getHeightSpacer();
|
||||||
boolean convertPdfToImage = request.isConvertPDFToImage();
|
|
||||||
|
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
|
||||||
@@ -106,12 +104,6 @@ public class WatermarkController {
|
|||||||
contentStream.close();
|
contentStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convertPdfToImage) {
|
|
||||||
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
|
||||||
document.close();
|
|
||||||
document = convertedPdf;
|
|
||||||
}
|
|
||||||
|
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.util.HashMap;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.util.Iterator;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -20,31 +23,27 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
|
||||||
import stirling.software.SPDF.model.*;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
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;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Slf4j
|
|
||||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
@Autowired SessionPersistentRegistry sessionPersistentRegistry;
|
private static final Logger logger = LoggerFactory.getLogger(AccountWebController.class);
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository; // Assuming you have a repository for user operations
|
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
|
||||||
// If the user is already authenticated, redirect them to the home page.
|
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
@@ -83,8 +82,6 @@ public class AccountWebController {
|
|||||||
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
|
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
|
||||||
model.addAttribute(
|
model.addAttribute(
|
||||||
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
"oAuth2Enabled", applicationProperties.getSecurity().getOAUTH2().getEnabled());
|
||||||
model.addAttribute(
|
|
||||||
"samlEnabled", applicationProperties.getSecurity().getSAML().getEnabled());
|
|
||||||
|
|
||||||
model.addAttribute("currentPage", "login");
|
model.addAttribute("currentPage", "login");
|
||||||
|
|
||||||
@@ -140,13 +137,6 @@ public class AccountWebController {
|
|||||||
break;
|
break;
|
||||||
case "invalid_id_token":
|
case "invalid_id_token":
|
||||||
erroroauth = "login.oauth2InvalidIdToken";
|
erroroauth = "login.oauth2InvalidIdToken";
|
||||||
break;
|
|
||||||
case "oauth2_admin_blocked_user":
|
|
||||||
erroroauth = "login.oauth2AdminBlockedUser";
|
|
||||||
break;
|
|
||||||
case "userIsDisabled":
|
|
||||||
erroroauth = "login.userIsDisabled";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -165,6 +155,9 @@ public class AccountWebController {
|
|||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository; // Assuming you have a repository for user operations
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@GetMapping("/addUsers")
|
@GetMapping("/addUsers")
|
||||||
public String showAddUserForm(
|
public String showAddUserForm(
|
||||||
@@ -173,13 +166,6 @@ public class AccountWebController {
|
|||||||
Iterator<User> iterator = allUsers.iterator();
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||||
|
|
||||||
// Map to store session information and user activity status
|
|
||||||
Map<String, Boolean> userSessions = new HashMap<>();
|
|
||||||
Map<String, Date> userLastRequest = new HashMap<>();
|
|
||||||
|
|
||||||
int activeUsers = 0;
|
|
||||||
int disabledUsers = 0;
|
|
||||||
|
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
User user = iterator.next();
|
User user = iterator.next();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
@@ -190,73 +176,9 @@ public class AccountWebController {
|
|||||||
break; // Break out of the inner loop once the user is removed
|
break; // Break out of the inner loop once the user is removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the user's session status and last request time
|
|
||||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
|
||||||
boolean hasActiveSession = false;
|
|
||||||
Date lastRequest = null;
|
|
||||||
|
|
||||||
Optional<SessionEntity> latestSession =
|
|
||||||
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
|
||||||
if (latestSession.isPresent()) {
|
|
||||||
SessionEntity sessionEntity = latestSession.get();
|
|
||||||
Date lastAccessedTime = sessionEntity.getLastRequest();
|
|
||||||
Instant now = Instant.now();
|
|
||||||
|
|
||||||
// Calculate session expiration and update session status accordingly
|
|
||||||
Instant expirationTime =
|
|
||||||
lastAccessedTime
|
|
||||||
.toInstant()
|
|
||||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
|
||||||
if (now.isAfter(expirationTime)) {
|
|
||||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
|
||||||
hasActiveSession = false;
|
|
||||||
} else {
|
|
||||||
hasActiveSession = !sessionEntity.isExpired();
|
|
||||||
}
|
|
||||||
|
|
||||||
lastRequest = sessionEntity.getLastRequest();
|
|
||||||
} else {
|
|
||||||
hasActiveSession = false;
|
|
||||||
lastRequest = new Date(0); // No session, set default last request time
|
|
||||||
}
|
|
||||||
|
|
||||||
userSessions.put(user.getUsername(), hasActiveSession);
|
|
||||||
userLastRequest.put(user.getUsername(), lastRequest);
|
|
||||||
|
|
||||||
if (hasActiveSession) {
|
|
||||||
activeUsers++;
|
|
||||||
}
|
|
||||||
if (!user.isEnabled()) {
|
|
||||||
disabledUsers++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort users by active status and last request date
|
|
||||||
List<User> sortedUsers =
|
|
||||||
allUsers.stream()
|
|
||||||
.sorted(
|
|
||||||
(u1, u2) -> {
|
|
||||||
boolean u1Active = userSessions.get(u1.getUsername());
|
|
||||||
boolean u2Active = userSessions.get(u2.getUsername());
|
|
||||||
|
|
||||||
if (u1Active && !u2Active) {
|
|
||||||
return -1;
|
|
||||||
} else if (!u1Active && u2Active) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
Date u1LastRequest =
|
|
||||||
userLastRequest.getOrDefault(
|
|
||||||
u1.getUsername(), new Date(0));
|
|
||||||
Date u2LastRequest =
|
|
||||||
userLastRequest.getOrDefault(
|
|
||||||
u2.getUsername(), new Date(0));
|
|
||||||
return u2LastRequest.compareTo(u1LastRequest);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
|
|
||||||
String deleteMessage = null;
|
String deleteMessage = null;
|
||||||
@@ -281,9 +203,6 @@ public class AccountWebController {
|
|||||||
case "invalidUsername":
|
case "invalidUsername":
|
||||||
addMessage = "invalidUsernameMessage";
|
addMessage = "invalidUsernameMessage";
|
||||||
break;
|
break;
|
||||||
case "invalidPassword":
|
|
||||||
addMessage = "invalidPasswordMessage";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -299,24 +218,16 @@ public class AccountWebController {
|
|||||||
case "downgradeCurrentUser":
|
case "downgradeCurrentUser":
|
||||||
changeMessage = "downgradeCurrentUserMessage";
|
changeMessage = "downgradeCurrentUserMessage";
|
||||||
break;
|
break;
|
||||||
case "disabledCurrentUser":
|
|
||||||
changeMessage = "disabledCurrentUserMessage";
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
changeMessage = messageType;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
model.addAttribute("changeMessage", changeMessage);
|
model.addAttribute("changeMessage", changeMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("users", sortedUsers);
|
model.addAttribute("users", allUsers);
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
model.addAttribute("roleDetails", roleDetails);
|
model.addAttribute("roleDetails", roleDetails);
|
||||||
model.addAttribute("userSessions", userSessions);
|
|
||||||
model.addAttribute("userLastRequest", userLastRequest);
|
|
||||||
model.addAttribute("totalUsers", allUsers.size());
|
|
||||||
model.addAttribute("activeUsers", activeUsers);
|
|
||||||
model.addAttribute("disabledUsers", disabledUsers);
|
|
||||||
return "addUsers";
|
return "addUsers";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +265,7 @@ public class AccountWebController {
|
|||||||
if (username != null) {
|
if (username != null) {
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user =
|
Optional<User> user =
|
||||||
userRepository.findByUsernameIgnoreCaseWithSettings(
|
userRepository.findByUsernameIgnoreCase(
|
||||||
username); // Assuming findByUsername method exists
|
username); // Assuming findByUsername method exists
|
||||||
if (!user.isPresent()) {
|
if (!user.isPresent()) {
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
@@ -367,7 +278,7 @@ public class AccountWebController {
|
|||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
// Handle JSON conversion error
|
||||||
log.error("exception", e);
|
logger.error("exception", e);
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import org.springframework.web.servlet.ModelAndView;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ConverterWebController {
|
public class ConverterWebController {
|
||||||
@@ -23,6 +21,14 @@ public class ConverterWebController {
|
|||||||
return "convert/book-to-pdf";
|
return "convert/book-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
||||||
|
@GetMapping("/pdf-to-book")
|
||||||
|
@Hidden
|
||||||
|
public String convertPdfToBookForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "pdf-to-book");
|
||||||
|
return "convert/pdf-to-book";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/img-to-pdf")
|
@GetMapping("/img-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertImgToPdfForm(Model model) {
|
public String convertImgToPdfForm(Model model) {
|
||||||
@@ -51,6 +57,13 @@ public class ConverterWebController {
|
|||||||
return "convert/url-to-pdf";
|
return "convert/url-to-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pdf-to-img")
|
||||||
|
@Hidden
|
||||||
|
public String pdfToimgForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "pdf-to-img");
|
||||||
|
return "convert/pdf-to-img";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/file-to-pdf")
|
@GetMapping("/file-to-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String convertToPdfForm(Model model) {
|
public String convertToPdfForm(Model model) {
|
||||||
@@ -60,23 +73,6 @@ public class ConverterWebController {
|
|||||||
|
|
||||||
// PDF TO......
|
// PDF TO......
|
||||||
|
|
||||||
@ConditionalOnExpression("#{bookAndHtmlFormatsInstalled}")
|
|
||||||
@GetMapping("/pdf-to-book")
|
|
||||||
@Hidden
|
|
||||||
public String convertPdfToBookForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "pdf-to-book");
|
|
||||||
return "convert/pdf-to-book";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/pdf-to-img")
|
|
||||||
@Hidden
|
|
||||||
public String pdfToimgForm(Model model) {
|
|
||||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
|
||||||
model.addAttribute("isPython", isPython);
|
|
||||||
model.addAttribute("currentPage", "pdf-to-img");
|
|
||||||
return "convert/pdf-to-img";
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/pdf-to-html")
|
@GetMapping("/pdf-to-html")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView pdfToHTML() {
|
public ModelAndView pdfToHTML() {
|
||||||
|
|||||||
@@ -310,11 +310,4 @@ public class GeneralWebController {
|
|||||||
model.addAttribute("currentPage", "auto-split-pdf");
|
model.addAttribute("currentPage", "auto-split-pdf");
|
||||||
return "auto-split-pdf";
|
return "auto-split-pdf";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/remove-image-pdf")
|
|
||||||
@Hidden
|
|
||||||
public String removeImagePdfForm(Model model) {
|
|
||||||
model.addAttribute("currentPage", "remove-image-pdf");
|
|
||||||
return "remove-image-pdf";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -15,15 +14,10 @@ import org.springframework.web.servlet.ModelAndView;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
public class OtherWebController {
|
public class OtherWebController {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
@GetMapping("/compress-pdf")
|
@GetMapping("/compress-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String compressPdfForm(Model model) {
|
public String compressPdfForm(Model model) {
|
||||||
@@ -35,8 +29,6 @@ public class OtherWebController {
|
|||||||
@Hidden
|
@Hidden
|
||||||
public ModelAndView extractImageScansForm() {
|
public ModelAndView extractImageScansForm() {
|
||||||
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
ModelAndView modelAndView = new ModelAndView("misc/extract-image-scans");
|
||||||
boolean isPython = CheckProgramInstall.isPythonAvailable();
|
|
||||||
modelAndView.addObject("isPython", isPython);
|
|
||||||
modelAndView.addObject("currentPage", "extract-image-scans");
|
modelAndView.addObject("currentPage", "extract-image-scans");
|
||||||
return modelAndView;
|
return modelAndView;
|
||||||
}
|
}
|
||||||
@@ -105,7 +97,7 @@ public class OtherWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAvailableTesseractLanguages() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
String tessdataDir = "/usr/share/tessdata";
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
if (files == null) {
|
if (files == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
@@ -12,11 +11,7 @@ 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.io.ClassPathResource;
|
|
||||||
import org.springframework.core.io.FileSystemResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
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;
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
@@ -135,7 +130,6 @@ public class ApplicationProperties {
|
|||||||
private Boolean csrfDisabled;
|
private Boolean csrfDisabled;
|
||||||
private InitialLogin initialLogin;
|
private InitialLogin initialLogin;
|
||||||
private OAUTH2 oauth2;
|
private OAUTH2 oauth2;
|
||||||
private SAML saml;
|
|
||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
private String loginMethod = "all";
|
private String loginMethod = "all";
|
||||||
@@ -180,14 +174,6 @@ public class ApplicationProperties {
|
|||||||
this.oauth2 = oauth2;
|
this.oauth2 = oauth2;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SAML getSAML() {
|
|
||||||
return saml != null ? saml : new SAML();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSAML(SAML saml) {
|
|
||||||
this.saml = saml;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Boolean getEnableLogin() {
|
public Boolean getEnableLogin() {
|
||||||
return enableLogin;
|
return enableLogin;
|
||||||
}
|
}
|
||||||
@@ -249,41 +235,12 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class SAML {
|
|
||||||
private Boolean enabled = false;
|
|
||||||
private String entityId;
|
|
||||||
private String registrationId;
|
|
||||||
private String spBaseUrl;
|
|
||||||
private String idpMetadataLocation;
|
|
||||||
private KeyStore keystore;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class KeyStore {
|
|
||||||
private String keystoreLocation;
|
|
||||||
private String keystorePassword;
|
|
||||||
private String keyAlias;
|
|
||||||
private String keyPassword;
|
|
||||||
private String realmCertificateAlias;
|
|
||||||
|
|
||||||
public Resource getKeystoreResource() {
|
|
||||||
if (keystoreLocation.startsWith("classpath:")) {
|
|
||||||
return new ClassPathResource(
|
|
||||||
keystoreLocation.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(keystoreLocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class OAUTH2 {
|
public static class OAUTH2 {
|
||||||
private Boolean enabled = false;
|
private Boolean enabled = false;
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private String clientId;
|
private String clientId;
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
private Boolean autoCreateUser = false;
|
private Boolean autoCreateUser = false;
|
||||||
private Boolean blockRegistration = false;
|
|
||||||
private String useAsUsername;
|
private String useAsUsername;
|
||||||
private Collection<String> scopes = new ArrayList<>();
|
private Collection<String> scopes = new ArrayList<>();
|
||||||
private String provider;
|
private String provider;
|
||||||
@@ -329,14 +286,6 @@ public class ApplicationProperties {
|
|||||||
this.autoCreateUser = autoCreateUser;
|
this.autoCreateUser = autoCreateUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getBlockRegistration() {
|
|
||||||
return blockRegistration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBlockRegistration(Boolean blockRegistration) {
|
|
||||||
this.blockRegistration = blockRegistration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUseAsUsername() {
|
public String getUseAsUsername() {
|
||||||
return useAsUsername;
|
return useAsUsername;
|
||||||
}
|
}
|
||||||
@@ -407,14 +356,10 @@ public class ApplicationProperties {
|
|||||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||||
+ ", autoCreateUser="
|
+ ", autoCreateUser="
|
||||||
+ autoCreateUser
|
+ autoCreateUser
|
||||||
+ ", blockRegistration="
|
|
||||||
+ blockRegistration
|
|
||||||
+ ", useAsUsername="
|
+ ", useAsUsername="
|
||||||
+ useAsUsername
|
+ useAsUsername
|
||||||
+ ", provider="
|
+ ", provider="
|
||||||
+ provider
|
+ provider
|
||||||
+ ", client="
|
|
||||||
+ client
|
|
||||||
+ ", scopes="
|
+ ", scopes="
|
||||||
+ scopes
|
+ scopes
|
||||||
+ "]";
|
+ "]";
|
||||||
@@ -484,15 +429,6 @@ public class ApplicationProperties {
|
|||||||
private boolean showUpdate;
|
private boolean showUpdate;
|
||||||
private Boolean showUpdateOnlyAdmin;
|
private Boolean showUpdateOnlyAdmin;
|
||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
private String tessdataDir;
|
|
||||||
|
|
||||||
public String getTessdataDir() {
|
|
||||||
return tessdataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTessdataDir(String tessdataDir) {
|
|
||||||
this.tessdataDir = tessdataDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCustomHTMLFiles() {
|
public boolean isCustomHTMLFiles() {
|
||||||
return customHTMLFiles;
|
return customHTMLFiles;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
import jakarta.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
@@ -13,9 +11,7 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "authorities")
|
@Table(name = "authorities")
|
||||||
public class Authority implements Serializable {
|
public class Authority {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
public Authority() {}
|
public Authority() {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package stirling.software.SPDF.model;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
import jakarta.persistence.Table;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Data
|
|
||||||
@Table(name = "sessions")
|
|
||||||
public class SessionEntity implements Serializable {
|
|
||||||
@Id private String sessionId;
|
|
||||||
|
|
||||||
@Lob private String principalName;
|
|
||||||
|
|
||||||
private Date lastRequest;
|
|
||||||
|
|
||||||
private boolean expired;
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,29 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.CascadeType;
|
||||||
|
import jakarta.persistence.CollectionTable;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.ElementCollection;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.MapKeyColumn;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
public class User implements Serializable {
|
public class User {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package stirling.software.SPDF.model.api;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
public class PDFExtractImagesRequest extends PDFWithImageFormatRequest {
|
|
||||||
|
|
||||||
@Schema(
|
|
||||||
description =
|
|
||||||
"Boolean to enable/disable the saving of duplicate images, true to enable duplicates")
|
|
||||||
private boolean allowDuplicates;
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ public class ConvertToImageRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The output image format",
|
description = "The output image format",
|
||||||
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"})
|
allowableValues = {"png", "jpeg", "jpg", "gif"})
|
||||||
private String imageFormat;
|
private String imageFormat;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import lombok.EqualsAndHashCode;
|
|||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
public class UrlToPdfRequest {
|
public class UrlToPdfRequest {
|
||||||
|
|
||||||
@Schema(
|
@Schema(description = "The input URL to be converted to a PDF file", required = true)
|
||||||
description = "The input URL to be converted to a PDF file",
|
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
private String urlInput;
|
private String urlInput;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class ContainsTextRequest extends PDFWithPageNums {
|
public class ContainsTextRequest extends PDFWithPageNums {
|
||||||
|
|
||||||
@Schema(description = "The text to check for", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "The text to check for", required = true)
|
||||||
private String text;
|
private String text;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class FileSizeRequest extends PDFComparison {
|
public class FileSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "File Size", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "File Size", required = true)
|
||||||
private String fileSize;
|
private String fileSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PageRotationRequest extends PDFComparison {
|
public class PageRotationRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "Rotation in degrees", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "Rotation in degrees", required = true)
|
||||||
private int rotation;
|
private int rotation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ import stirling.software.SPDF.model.api.PDFComparison;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PageSizeRequest extends PDFComparison {
|
public class PageSizeRequest extends PDFComparison {
|
||||||
|
|
||||||
@Schema(description = "Standard Page Size", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "Standard Page Size", required = true)
|
||||||
private String standardPageSize;
|
private String standardPageSize;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ public class OverlayPdfsRequest extends PDFFile {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts",
|
"The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts",
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
required = true)
|
||||||
private String overlayMode;
|
private String overlayMode;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.",
|
"An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.",
|
||||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
|
required = false)
|
||||||
private int[] counts;
|
private int[] counts;
|
||||||
|
|
||||||
@Schema(description = "Overlay position 0 is Foregound, 1 is Background")
|
@Schema(description = "Overlay position 0 is Foregound, 1 is Background")
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ public class SplitPdfBySizeOrCountRequest extends PDFFile {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Determines the type of split: 0 for size, 1 for page count, 2 for document count",
|
"Determines the type of split: 0 for size, 1 for page count, 2 for document count",
|
||||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
required = false,
|
||||||
defaultValue = "0")
|
defaultValue = "0")
|
||||||
private int splitType;
|
private int splitType;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')",
|
"Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')",
|
||||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
required = false,
|
||||||
defaultValue = "10MB")
|
defaultValue = "10MB")
|
||||||
private String splitValue;
|
private String splitValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class AddStampRequest extends PDFWithPageNums {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description = "The stamp type (text or image)",
|
description = "The stamp type (text or image)",
|
||||||
allowableValues = {"text", "image"},
|
allowableValues = {"text", "image"},
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
required = true)
|
||||||
private String stampType;
|
private String stampType;
|
||||||
|
|
||||||
@Schema(description = "The stamp text")
|
@Schema(description = "The stamp text")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class AutoSplitPdfRequest extends PDFFile {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Flag indicating if the duplex mode is active, where the page after the divider also gets removed.",
|
"Flag indicating if the duplex mode is active, where the page after the divider also gets removed.",
|
||||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
required = false,
|
||||||
defaultValue = "false")
|
defaultValue = "false")
|
||||||
private boolean duplexMode;
|
private boolean duplexMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class ExtractHeaderRequest extends PDFFile {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description =
|
description =
|
||||||
"Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.",
|
"Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.",
|
||||||
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
|
required = false,
|
||||||
defaultValue = "false")
|
defaultValue = "false")
|
||||||
private boolean useFirstTextAsFallback;
|
private boolean useFirstTextAsFallback;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode
|
@EqualsAndHashCode
|
||||||
public class ExtractImageScansRequest {
|
public class ExtractImageScansRequest {
|
||||||
@Schema(
|
@Schema(description = "The input file containing image scans", required = true)
|
||||||
description = "The input file containing image scans",
|
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
private MultipartFile fileInput;
|
private MultipartFile fileInput;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import stirling.software.SPDF.model.api.PDFFile;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PrintFileRequest extends PDFFile {
|
public class PrintFileRequest extends PDFFile {
|
||||||
|
|
||||||
@Schema(
|
@Schema(description = "Name of printer to match against", required = true)
|
||||||
description = "Name of printer to match against",
|
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
private String printerName;
|
private String printerName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class AddWatermarkRequest extends PDFFile {
|
|||||||
@Schema(
|
@Schema(
|
||||||
description = "The watermark type (text or image)",
|
description = "The watermark type (text or image)",
|
||||||
allowableValues = {"text", "image"},
|
allowableValues = {"text", "image"},
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
required = true)
|
||||||
private String watermarkType;
|
private String watermarkType;
|
||||||
|
|
||||||
@Schema(description = "The watermark text")
|
@Schema(description = "The watermark text")
|
||||||
@@ -44,7 +44,4 @@ public class AddWatermarkRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(description = "The height spacer between watermark elements", example = "50")
|
@Schema(description = "The height spacer between watermark elements", example = "50")
|
||||||
private int heightSpacer;
|
private int heightSpacer;
|
||||||
|
|
||||||
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
|
||||||
private boolean convertPDFToImage;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import stirling.software.SPDF.model.api.PDFFile;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class PDFPasswordRequest extends PDFFile {
|
public class PDFPasswordRequest extends PDFFile {
|
||||||
|
|
||||||
@Schema(
|
@Schema(description = "The password of the PDF file", required = true)
|
||||||
description = "The password of the PDF file",
|
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
private String password;
|
private String password;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,7 @@ import stirling.software.SPDF.model.api.PDFFile;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class RedactPdfRequest extends PDFFile {
|
public class RedactPdfRequest extends PDFFile {
|
||||||
|
|
||||||
@Schema(
|
@Schema(description = "List of text to redact from the PDF", type = "string", required = true)
|
||||||
description = "List of text to redact from the PDF",
|
|
||||||
type = "string",
|
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
|
||||||
private String listOfText;
|
private String listOfText;
|
||||||
|
|
||||||
@Schema(description = "Whether to use regex for the listOfText", defaultValue = "false")
|
@Schema(description = "Whether to use regex for the listOfText", defaultValue = "false")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user