Compare commits
74 Commits
moveDocsTo
...
csrf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74a1d83f8d | ||
|
|
1ed1b17510 | ||
|
|
7a431f509e | ||
|
|
e1a320ac37 | ||
|
|
ded3915424 | ||
|
|
a979b526c4 | ||
|
|
5272e4c7ff | ||
|
|
e95cbecab4 | ||
|
|
85d2f6f0cc | ||
|
|
5ef8c6be55 | ||
|
|
f879f5d533 | ||
|
|
f8e1ce6a7b | ||
|
|
e49ca245e5 | ||
|
|
198fc1ced3 | ||
|
|
ad50e90a03 | ||
|
|
a4afe5b708 | ||
|
|
d1ed70146c | ||
|
|
fe84128297 | ||
|
|
690527aabd | ||
|
|
2b27cbbd4c | ||
|
|
544e838f7c | ||
|
|
f379c27bd7 | ||
|
|
8d4c762c7e | ||
|
|
bad5a2bc8b | ||
|
|
e17dfcb369 | ||
|
|
6d73f01107 | ||
|
|
c6c1dceaa2 | ||
|
|
59c28f10f9 | ||
|
|
f5afce8fc1 | ||
|
|
41dce06804 | ||
|
|
7382efd80d | ||
|
|
98d4443ebd | ||
|
|
ddbef1c82b | ||
|
|
52bf4381ab | ||
|
|
d8a4f44862 | ||
|
|
ed633616e7 | ||
|
|
f08f8c734b | ||
|
|
22af79a279 | ||
|
|
79f6598508 | ||
|
|
e754e6012a | ||
|
|
3227da55e0 | ||
|
|
709a79c3d9 | ||
|
|
77bb15bc8b | ||
|
|
beaa86cbf9 | ||
|
|
b8303e3860 | ||
|
|
5ba98e4411 | ||
|
|
116dfcc065 | ||
|
|
2d76927b3c | ||
|
|
41c269f208 | ||
|
|
38633d4db1 | ||
|
|
0ff45c656a | ||
|
|
bc282c6aa5 | ||
|
|
50575bc80b | ||
|
|
6f04f01c2b | ||
|
|
e80eaaa6d1 | ||
|
|
01288dafe8 | ||
|
|
e3c7b6f955 | ||
|
|
875f5a85ef | ||
|
|
ef174a1e8a | ||
|
|
958c214605 | ||
|
|
56ab9c3eee | ||
|
|
7b768db969 | ||
|
|
36c4d8552d | ||
|
|
4544fb7211 | ||
|
|
807639308a | ||
|
|
49fb634690 | ||
|
|
715efca25d | ||
|
|
a4c988c7b2 | ||
|
|
f45de05c99 | ||
|
|
d5faddbc85 | ||
|
|
5e173b92d4 | ||
|
|
574c474804 | ||
|
|
2e23149090 | ||
|
|
7be8db7832 |
9
.github/labeler-config.yml
vendored
9
.github/labeler-config.yml
vendored
@@ -9,6 +9,7 @@ Front End:
|
|||||||
- 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/resources/static/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/UI/**/*'
|
||||||
|
|
||||||
Java:
|
Java:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
@@ -29,6 +30,7 @@ Security:
|
|||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
|
||||||
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
|
||||||
|
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/BackupNotFoundException.java'
|
||||||
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||||
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
|
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
|
||||||
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
|
||||||
@@ -49,12 +51,17 @@ Documentation:
|
|||||||
|
|
||||||
Docker:
|
Docker:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
- any-glob-to-any-file: '.github/workflows/build.yml'
|
||||||
|
- any-glob-to-any-file: '.github/workflows/push-docker.yml'
|
||||||
- any-glob-to-any-file: 'Dockerfile'
|
- any-glob-to-any-file: 'Dockerfile'
|
||||||
- any-glob-to-any-file: 'Dockerfile-*'
|
- any-glob-to-any-file: 'Dockerfile.*'
|
||||||
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
|
||||||
|
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
|
||||||
- any-glob-to-any-file: 'scripts/init.sh'
|
- any-glob-to-any-file: 'scripts/init.sh'
|
||||||
- any-glob-to-any-file: 'scripts/init-without-ocr.sh'
|
- any-glob-to-any-file: 'scripts/init-without-ocr.sh'
|
||||||
- any-glob-to-any-file: 'scripts/installFonts.sh'
|
- any-glob-to-any-file: 'scripts/installFonts.sh'
|
||||||
|
- any-glob-to-any-file: 'test.sh'
|
||||||
|
- any-glob-to-any-file: 'test2.sh'
|
||||||
|
|
||||||
Test:
|
Test:
|
||||||
- changed-files:
|
- changed-files:
|
||||||
|
|||||||
16
.github/workflows/PR-Demo-Comment.yml
vendored
16
.github/workflows/PR-Demo-Comment.yml
vendored
@@ -4,9 +4,15 @@ on:
|
|||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-comment:
|
check-comment:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
if: |
|
if: |
|
||||||
github.event.issue.pull_request &&
|
github.event.issue.pull_request &&
|
||||||
(
|
(
|
||||||
@@ -20,7 +26,8 @@ jobs:
|
|||||||
github.event.comment.user.login == 'Ludy87' ||
|
github.event.comment.user.login == 'Ludy87' ||
|
||||||
github.event.comment.user.login == 'LaserKaspar' ||
|
github.event.comment.user.login == 'LaserKaspar' ||
|
||||||
github.event.comment.user.login == 'sbplat' ||
|
github.event.comment.user.login == 'sbplat' ||
|
||||||
github.event.comment.user.login == 'reecebrowne'
|
github.event.comment.user.login == 'reecebrowne' ||
|
||||||
|
github.event.comment.user.login == 'DarioGii'
|
||||||
)
|
)
|
||||||
outputs:
|
outputs:
|
||||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||||
@@ -68,6 +75,9 @@ jobs:
|
|||||||
deploy-pr:
|
deploy-pr:
|
||||||
needs: check-comment
|
needs: check-comment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
@@ -98,7 +108,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: |
|
||||||
|
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||||
|
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
|
||||||
|
|||||||
3
.github/workflows/PR-Demo-cleanup.yml
vendored
3
.github/workflows/PR-Demo-cleanup.yml
vendored
@@ -4,7 +4,8 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, closed]
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
||||||
|
|||||||
3
.github/workflows/auto-labeler.yml
vendored
3
.github/workflows/auto-labeler.yml
vendored
@@ -3,7 +3,8 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
|
|||||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -6,13 +6,15 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
actions: read
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -45,6 +47,17 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
DOCKER_ENABLE_SECURITY: true
|
DOCKER_ENABLE_SECURITY: true
|
||||||
|
|
||||||
|
- name: Upload Test Reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||||
|
with:
|
||||||
|
name: test-reports-jdk-${{ matrix.jdk-version }}
|
||||||
|
path: |
|
||||||
|
build/reports/tests/
|
||||||
|
build/test-results/
|
||||||
|
build/reports/problems/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
docker-compose-tests:
|
docker-compose-tests:
|
||||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||||
# (github.event_name == 'pull_request' &&
|
# (github.event_name == 'pull_request' &&
|
||||||
@@ -82,7 +95,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Docker Compose
|
- name: Install Docker Compose
|
||||||
run: |
|
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 curl -SL "https://github.com/docker/compose/releases/download/v2.32.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||||
sudo chmod +x /usr/local/bin/docker-compose
|
sudo chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
@@ -92,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Pip requirements
|
- name: Pip requirements
|
||||||
run: |
|
run: |
|
||||||
pip install -r ./cucumber/requirements.txt
|
pip install --require-hashes -r ./cucumber/requirements.txt
|
||||||
|
|
||||||
- name: Run Docker Compose Tests
|
- name: Run Docker Compose Tests
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
173
.github/workflows/check_properties.yml
vendored
173
.github/workflows/check_properties.yml
vendored
@@ -8,12 +8,14 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # Allow read access to repository content
|
contents: read # Allow read access to repository content
|
||||||
issues: write # Allow posting comments on issues/PRs
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-files:
|
check-files:
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write # Allow posting comments on issues/PRs
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
@@ -26,7 +28,28 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.12"
|
||||||
|
|
||||||
|
- name: Get PR data
|
||||||
|
id: get-pr-data
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = context.payload.pull_request.number;
|
||||||
|
const repoOwner = context.payload.repository.owner.login;
|
||||||
|
const repoName = context.payload.repository.name;
|
||||||
|
const branch = context.payload.pull_request.head.ref;
|
||||||
|
|
||||||
|
console.log(`PR Number: ${prNumber}`);
|
||||||
|
console.log(`Repo Owner: ${repoOwner}`);
|
||||||
|
console.log(`Repo Name: ${repoName}`);
|
||||||
|
console.log(`Branch: ${branch}`);
|
||||||
|
|
||||||
|
core.setOutput("pr_number", prNumber);
|
||||||
|
core.setOutput("repo_owner", repoOwner);
|
||||||
|
core.setOutput("repo_name", repoName);
|
||||||
|
core.setOutput("branch", branch);
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Fetch PR changed files
|
- name: Fetch PR changed files
|
||||||
id: fetch-pr-changes
|
id: fetch-pr-changes
|
||||||
@@ -34,55 +57,114 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "Fetching PR changed files..."
|
echo "Fetching PR changed files..."
|
||||||
|
|
||||||
gh repo set-default ${{ github.event.pull_request.head.repo.full_name }} # Set the fork repository as default
|
|
||||||
|
|
||||||
# Fetch the list of changed files in the PR
|
|
||||||
echo "Getting list of changed files from PR..."
|
echo "Getting list of changed files from PR..."
|
||||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > changed_files.txt # Filter only matching property files
|
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > changed_files.txt # Filter only matching property files
|
||||||
|
|
||||||
- name: Determine reference file
|
- name: Determine reference file test
|
||||||
id: determine-file
|
id: determine-file
|
||||||
env:
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
with:
|
||||||
run: |
|
script: |
|
||||||
echo "Determining reference file..."
|
const fs = require("fs");
|
||||||
REPO_OWNER=$(gh pr view ${{ github.event.pull_request.number }} --json author -q '.author.login') # Get PR author's username
|
const path = require("path");
|
||||||
REPO_NAME=$(gh pr view ${{ github.event.pull_request.number }} --json headRepository -q '.headRepository.name') # Get PR repository name
|
|
||||||
BRANCH=$(gh pr view ${{ github.event.pull_request.number }} --json headRefName -q '.headRefName') # Get PR branch name
|
|
||||||
|
|
||||||
mkdir -p pr-branch # Create a directory for PR files
|
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }};
|
||||||
|
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}";
|
||||||
|
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
|
||||||
|
|
||||||
# Download the content of each changed file
|
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
|
||||||
while IFS= read -r file; do
|
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
|
||||||
mkdir -p "pr-branch/$(dirname "$file")" # Create directories for files
|
const branch = "${{ steps.get-pr-data.outputs.branch }}";
|
||||||
gh api repos/$REPO_OWNER/$REPO_NAME/contents/$file?ref=$BRANCH --jq '.content' | base64 -d > "pr-branch/src/main/resources/$(basename "$file")" # Save decoded file content
|
|
||||||
done < changed_files.txt
|
|
||||||
|
|
||||||
# Generate a list of files without the "pr-branch/" prefix
|
console.log(`Determining reference file for PR #${prNumber}`);
|
||||||
find pr-branch/ -type f | awk -F'pr-branch/' '{print $2}' > file_list.txt
|
|
||||||
|
|
||||||
mapfile -t FILES_LIST < file_list.txt # Read the file list into an array
|
// Validate inputs
|
||||||
FILES_LIST_STR="${FILES_LIST[*]}" # Join array into a space-separated string
|
const validateInput = (input, regex, name) => {
|
||||||
echo "FILES_LIST=${FILES_LIST_STR}" >> $GITHUB_ENV # Export the file list to the environment
|
if (!regex.test(input)) {
|
||||||
echo "Changed files: ${FILES_LIST_STR}"
|
throw new Error(`Invalid ${name}: ${input}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cat file_list.txt # Display the file list
|
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
|
||||||
|
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
|
||||||
|
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
|
||||||
|
|
||||||
# Determine which reference file to use
|
// Get the list of changed files in the PR
|
||||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
const { data: files } = await github.rest.pulls.listFiles({
|
||||||
echo "Using PR branch reference file"
|
owner: repoOwner,
|
||||||
REFERENCE_FILE="pr-branch-messages_en_GB.properties"
|
repo: repoName,
|
||||||
gh api repos/$REPO_OWNER/$REPO_NAME/contents/src/main/resources/messages_en_GB.properties?ref=${{ github.event.pull_request.head.ref }} \
|
pull_number: prNumber,
|
||||||
--jq '.content' | base64 -d > $REFERENCE_FILE # Save PR branch reference file
|
});
|
||||||
else
|
|
||||||
echo "Using main branch reference file"
|
|
||||||
REFERENCE_FILE="main-branch-messages_en_GB.properties"
|
|
||||||
gh api repos/Ludy87/test_java/contents/src/main/resources/messages_en_GB.properties?ref=main \
|
|
||||||
--jq '.content' | base64 -d > $REFERENCE_FILE # Save main branch reference file
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "REFERENCE_FILE=$REFERENCE_FILE" >> $GITHUB_ENV # Export reference file path to the environment
|
// Filter for relevant files based on the PR changes
|
||||||
|
const changedFiles = files
|
||||||
|
.map(file => file.filename)
|
||||||
|
.filter(file => /^src\/main\/resources\/messages_[a-zA-Z_]+\.properties$/.test(file));
|
||||||
|
|
||||||
|
console.log("Changed files:", changedFiles);
|
||||||
|
|
||||||
|
// Create a temporary directory for PR files
|
||||||
|
const tempDir = "pr-branch";
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and save each changed file
|
||||||
|
for (const file of changedFiles) {
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: prRepoOwner,
|
||||||
|
repo: prRepoName,
|
||||||
|
path: file,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
const filePath = path.join(tempDir, file);
|
||||||
|
const dirPath = path.dirname(filePath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, content);
|
||||||
|
console.log(`Saved file: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output the list of changed files for further processing
|
||||||
|
const fileList = changedFiles.join(" ");
|
||||||
|
core.exportVariable("FILES_LIST", fileList);
|
||||||
|
console.log("Files saved and listed in FILES_LIST.");
|
||||||
|
|
||||||
|
// Determine reference file
|
||||||
|
let referenceFilePath;
|
||||||
|
if (changedFiles.includes("src/main/resources/messages_en_GB.properties")) {
|
||||||
|
console.log("Using PR branch reference file.");
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: prRepoOwner,
|
||||||
|
repo: prRepoName,
|
||||||
|
path: "src/main/resources/messages_en_GB.properties",
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "pr-branch-messages_en_GB.properties";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
} else {
|
||||||
|
console.log("Using main branch reference file.");
|
||||||
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
|
owner: repoOwner,
|
||||||
|
repo: repoName,
|
||||||
|
path: "src/main/resources/messages_en_GB.properties",
|
||||||
|
ref: "main",
|
||||||
|
});
|
||||||
|
|
||||||
|
referenceFilePath = "main-branch-messages_en_GB.properties";
|
||||||
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Reference file path: ${referenceFilePath}`);
|
||||||
|
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||||
|
|
||||||
- name: Run Python script to check files
|
- name: Run Python script to check files
|
||||||
id: run-check
|
id: run-check
|
||||||
@@ -92,7 +174,8 @@ jobs:
|
|||||||
--actor ${{ github.event.pull_request.user.login }} \
|
--actor ${{ github.event.pull_request.user.login }} \
|
||||||
--reference-file "${REFERENCE_FILE}" \
|
--reference-file "${REFERENCE_FILE}" \
|
||||||
--branch "pr-branch" \
|
--branch "pr-branch" \
|
||||||
--files "${FILES_LIST[@]}" > result.txt || true
|
--files "${FILES_LIST[@]}" > result.txt
|
||||||
|
continue-on-error: true # Continue the job even if this step fails
|
||||||
|
|
||||||
- name: Capture output
|
- name: Capture output
|
||||||
id: capture-output
|
id: capture-output
|
||||||
@@ -124,13 +207,13 @@ jobs:
|
|||||||
script: |
|
script: |
|
||||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||||
const prNumber = context.issue.number;
|
const issueNumber = context.issue.number;
|
||||||
|
|
||||||
// Find existing comment
|
// Find existing comment
|
||||||
const comments = await github.rest.issues.listComments({
|
const comments = await github.rest.issues.listComments({
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
issue_number: prNumber
|
issue_number: issueNumber
|
||||||
});
|
});
|
||||||
|
|
||||||
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
||||||
@@ -152,7 +235,7 @@ jobs:
|
|||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
issue_number: prNumber,
|
issue_number: issueNumber,
|
||||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||||
});
|
});
|
||||||
console.log("Created new comment.");
|
console.log("Created new comment.");
|
||||||
|
|||||||
5
.github/workflows/licenses-update.yml
vendored
5
.github/workflows/licenses-update.yml
vendored
@@ -7,7 +7,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "build.gradle"
|
- "build.gradle"
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate-license-report:
|
generate-license-report:
|
||||||
@@ -52,7 +53,7 @@ jobs:
|
|||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "Update 3rd Party Licenses"
|
commit-message: "Update 3rd Party Licenses"
|
||||||
|
|||||||
3
.github/workflows/manage-label.yml
vendored
3
.github/workflows/manage-label.yml
vendored
@@ -4,7 +4,8 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "30 20 * * *"
|
- cron: "30 20 * * *"
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
labeler:
|
labeler:
|
||||||
|
|||||||
5
.github/workflows/multiOSReleases.yml
vendored
5
.github/workflows/multiOSReleases.yml
vendored
@@ -5,7 +5,8 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-installers:
|
build-installers:
|
||||||
@@ -42,7 +43,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.12
|
||||||
|
|
||||||
# Install Windows dependencies
|
# Install Windows dependencies
|
||||||
- name: Install WiX Toolset
|
- name: Install WiX Toolset
|
||||||
|
|||||||
7
.github/workflows/pre_commit.yml
vendored
7
.github/workflows/pre_commit.yml
vendored
@@ -4,7 +4,8 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update:
|
update:
|
||||||
@@ -19,7 +20,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.12
|
python-version: 3.12
|
||||||
- name: Run Pre-Commit Hooks
|
- name: Run Pre-Commit Hooks
|
||||||
@@ -35,7 +36,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":file_folder: pre-commit
|
git diff --staged --quiet || git commit -m ":file_folder: pre-commit
|
||||||
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
|
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "ci: 🤖 format everything with pre-commit"
|
commit-message: "ci: 🤖 format everything with pre-commit"
|
||||||
|
|||||||
7
.github/workflows/push-docker.yml
vendored
7
.github/workflows/push-docker.yml
vendored
@@ -9,14 +9,13 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||||
@@ -33,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.12
|
||||||
|
|
||||||
- name: Run Gradle Command
|
- name: Run Gradle Command
|
||||||
run: ./gradlew clean build
|
run: ./gradlew clean build
|
||||||
@@ -42,7 +41,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@v3.7.0
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||||
with:
|
with:
|
||||||
cosign-release: 'v2.4.1'
|
cosign-release: 'v2.4.1'
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/releaseArtifacts.yml
vendored
5
.github/workflows/releaseArtifacts.yml
vendored
@@ -5,7 +5,8 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [created]
|
types: [created]
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
@@ -37,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.7
|
gradle-version: 8.12
|
||||||
|
|
||||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||||
run: ./gradlew clean createExe
|
run: ./gradlew clean createExe
|
||||||
|
|||||||
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -5,7 +5,8 @@ on:
|
|||||||
- cron: "30 0 * * *"
|
- cron: "30 0 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|||||||
3
.github/workflows/swagger.yml
vendored
3
.github/workflows/swagger.yml
vendored
@@ -6,7 +6,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push:
|
push:
|
||||||
|
|||||||
7
.github/workflows/sync_files.yml
vendored
7
.github/workflows/sync_files.yml
vendored
@@ -9,7 +9,8 @@ on:
|
|||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
- "scripts/ignore_translation.toml"
|
- "scripts/ignore_translation.toml"
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-readme:
|
sync-readme:
|
||||||
@@ -27,7 +28,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.12"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install tomlkit
|
run: pip install tomlkit
|
||||||
- name: Sync README
|
- name: Sync README
|
||||||
@@ -42,7 +43,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":memo: Sync README
|
git diff --staged --quiet || git commit -m ":memo: Sync README
|
||||||
> Made via sync_files.yml" || echo "no changes"
|
> Made via sync_files.yml" || echo "no changes"
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
|
|||||||
7
.github/workflows/update-translations.yml
vendored
7
.github/workflows/update-translations.yml
vendored
@@ -6,7 +6,8 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "src/main/resources/messages_en_GB.properties"
|
- "src/main/resources/messages_en_GB.properties"
|
||||||
|
|
||||||
permissions: read-all
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-translations-main:
|
update-translations-main:
|
||||||
@@ -27,7 +28,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.12"
|
||||||
|
|
||||||
- name: Run Python script to check files
|
- name: Run Python script to check files
|
||||||
id: run-check
|
id: run-check
|
||||||
@@ -50,7 +51,7 @@ jobs:
|
|||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
|
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "Update translation files"
|
commit-message: "Update translation files"
|
||||||
|
|||||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -146,16 +146,34 @@ out/
|
|||||||
# cucumber
|
# cucumber
|
||||||
/cucumber/reports/**
|
/cucumber/reports/**
|
||||||
|
|
||||||
# Certs
|
# Certs and Security Files
|
||||||
*.p12
|
*.p12
|
||||||
|
*.pk8
|
||||||
*.pem
|
*.pem
|
||||||
*.crt
|
*.crt
|
||||||
*.cer
|
*.cer
|
||||||
|
*.cert
|
||||||
*.der
|
*.der
|
||||||
*.key
|
*.key
|
||||||
*.csr
|
*.csr
|
||||||
|
*.kdbx
|
||||||
|
*.jks
|
||||||
|
*.asc
|
||||||
|
|
||||||
|
# SSH Keys
|
||||||
|
*.pub
|
||||||
|
*.priv
|
||||||
|
id_rsa
|
||||||
|
id_rsa.pub
|
||||||
|
id_ecdsa
|
||||||
|
id_ecdsa.pub
|
||||||
|
id_ed25519
|
||||||
|
id_ed25519.pub
|
||||||
|
.ssh/
|
||||||
|
*ssh
|
||||||
|
|
||||||
# cache
|
# cache
|
||||||
|
.cache
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build the application
|
# Build the application
|
||||||
FROM gradle:8.11-jdk17 AS build
|
FROM gradle:8.12-jdk17 AS build
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
76
README.md
76
README.md
@@ -4,9 +4,9 @@
|
|||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
[](https://discord.gg/HYmhKj45pU)
|
[](https://discord.gg/HYmhKj45pU)
|
||||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
|
[](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
|
|
||||||
|
|
||||||
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||||
[](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)
|
||||||
|
|
||||||
@@ -14,8 +14,9 @@
|
|||||||
|
|
||||||
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
||||||
|
|
||||||
|
Homepage: [https://stirlingpdf.com](https://stirlingpdf.com)
|
||||||
|
|
||||||
All information available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
|
All documentation available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -116,44 +117,45 @@ Stirling-PDF currently supports 38 languages!
|
|||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| -------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Persian (فارسی) (fa_IR) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|
||||||
|
|
||||||
## Stirling PDF Enterprise
|
## Stirling PDF Enterprise
|
||||||
|
|||||||
42
build.gradle
42
build.gradle
@@ -27,7 +27,7 @@ ext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.36.5"
|
version = "0.36.6"
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
@@ -52,13 +52,15 @@ sourceSets {
|
|||||||
java {
|
java {
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||||
exclude "stirling/software/SPDF/config/security/**"
|
exclude "stirling/software/SPDF/config/security/**"
|
||||||
exclude "stirling/software/SPDF/controller/api/UserController.java"
|
|
||||||
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
|
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/api/UserController.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java"
|
||||||
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/AttemptCounter.java"
|
||||||
exclude "stirling/software/SPDF/model/Authority.java"
|
exclude "stirling/software/SPDF/model/Authority.java"
|
||||||
|
exclude "stirling/software/SPDF/model/BackupNotFoundException.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/SessionEntity.java"
|
||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
@@ -69,7 +71,29 @@ sourceSets {
|
|||||||
exclude "stirling/software/SPDF/UI/impl/**"
|
exclude "stirling/software/SPDF/UI/impl/**"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
java {
|
||||||
|
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||||
|
exclude "stirling/software/SPDF/config/security/**"
|
||||||
|
exclude "stirling/software/SPDF/controller/api/UserControllerTest.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java"
|
||||||
|
exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/AttemptCounterTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/AuthorityTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/PersistentLoginTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/SessionEntityTest.java"
|
||||||
|
exclude "stirling/software/SPDF/model/UserTest.java"
|
||||||
|
exclude "stirling/software/SPDF/repository/**"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||||
|
exclude "stirling/software/SPDF/UI/impl/**"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,11 +147,13 @@ jpackage {
|
|||||||
windows {
|
windows {
|
||||||
launcherAsService = false
|
launcherAsService = false
|
||||||
appVersion = project.version
|
appVersion = project.version
|
||||||
|
|
||||||
winConsole = false
|
winConsole = false
|
||||||
winDirChooser = true
|
winMenu = true // Creates start menu entry
|
||||||
winMenu = true
|
winShortcut = true // Creates desktop shortcut
|
||||||
winShortcut = true
|
winShortcutPrompt = true // Lets user choose whether to create shortcuts
|
||||||
winPerUserInstall = true
|
winDirChooser = true // Allows users to choose installation directory
|
||||||
|
winPerUserInstall = false
|
||||||
winMenuGroup = "Stirling Software"
|
winMenuGroup = "Stirling Software"
|
||||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||||
@@ -257,7 +283,7 @@ spotless {
|
|||||||
// rules=['unused-dependency']
|
// rules=['unused-dependency']
|
||||||
// }
|
// }
|
||||||
tasks.wrapper {
|
tasks.wrapper {
|
||||||
gradleVersion = "8.7"
|
gradleVersion = "8.12"
|
||||||
}
|
}
|
||||||
//tasks.withType(JavaCompile) {
|
//tasks.withType(JavaCompile) {
|
||||||
// options.compilerArgs << "-Xlint:deprecation"
|
// options.compilerArgs << "-Xlint:deprecation"
|
||||||
@@ -297,10 +323,12 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
||||||
|
implementation "org.springframework:spring-jdbc:6.2.1"
|
||||||
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
// Don't upgrade h2database
|
// Don't upgrade h2database
|
||||||
runtimeOnly "com.h2database:h2:2.3.232"
|
runtimeOnly "com.h2database:h2:2.3.232"
|
||||||
|
runtimeOnly "org.postgresql:postgresql:42.7.4"
|
||||||
constraints {
|
constraints {
|
||||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Feature: API Validation
|
|||||||
| pageNumbers | file_count |
|
| pageNumbers | file_count |
|
||||||
| 1,3,5-9 | 8 |
|
| 1,3,5-9 | 8 |
|
||||||
| all | 20 |
|
| all | 20 |
|
||||||
| 2n+1 | 11 |
|
| 2n+1 | 10 |
|
||||||
| 3n | 7 |
|
| 3n | 7 |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
cucumber/requirements.in
Normal file
5
cucumber/requirements.in
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
behave
|
||||||
|
requests
|
||||||
|
PyPDF2
|
||||||
|
reportlab
|
||||||
|
PyCryptodome
|
||||||
@@ -1,5 +1,255 @@
|
|||||||
behave
|
#
|
||||||
requests
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
PyPDF2
|
# by the following command:
|
||||||
reportlab
|
#
|
||||||
PyCryptodome
|
# pip-compile --generate-hashes --output-file='cucumber\requirements.txt' 'cucumber\requirements.in'
|
||||||
|
#
|
||||||
|
behave==1.2.6 \
|
||||||
|
--hash=sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86 \
|
||||||
|
--hash=sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c
|
||||||
|
# via -r cucumber\requirements.in
|
||||||
|
certifi==2024.12.14 \
|
||||||
|
--hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \
|
||||||
|
--hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db
|
||||||
|
# via requests
|
||||||
|
chardet==5.2.0 \
|
||||||
|
--hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \
|
||||||
|
--hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970
|
||||||
|
# via reportlab
|
||||||
|
charset-normalizer==3.4.1 \
|
||||||
|
--hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \
|
||||||
|
--hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \
|
||||||
|
--hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \
|
||||||
|
--hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \
|
||||||
|
--hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \
|
||||||
|
--hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \
|
||||||
|
--hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \
|
||||||
|
--hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \
|
||||||
|
--hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \
|
||||||
|
--hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \
|
||||||
|
--hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \
|
||||||
|
--hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \
|
||||||
|
--hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \
|
||||||
|
--hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \
|
||||||
|
--hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \
|
||||||
|
--hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \
|
||||||
|
--hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \
|
||||||
|
--hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \
|
||||||
|
--hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \
|
||||||
|
--hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \
|
||||||
|
--hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \
|
||||||
|
--hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \
|
||||||
|
--hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \
|
||||||
|
--hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \
|
||||||
|
--hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \
|
||||||
|
--hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \
|
||||||
|
--hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \
|
||||||
|
--hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
|
||||||
|
--hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \
|
||||||
|
--hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \
|
||||||
|
--hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \
|
||||||
|
--hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \
|
||||||
|
--hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \
|
||||||
|
--hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \
|
||||||
|
--hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \
|
||||||
|
--hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \
|
||||||
|
--hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \
|
||||||
|
--hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \
|
||||||
|
--hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \
|
||||||
|
--hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \
|
||||||
|
--hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \
|
||||||
|
--hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \
|
||||||
|
--hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \
|
||||||
|
--hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \
|
||||||
|
--hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \
|
||||||
|
--hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \
|
||||||
|
--hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \
|
||||||
|
--hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \
|
||||||
|
--hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \
|
||||||
|
--hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \
|
||||||
|
--hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \
|
||||||
|
--hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \
|
||||||
|
--hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \
|
||||||
|
--hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \
|
||||||
|
--hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \
|
||||||
|
--hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \
|
||||||
|
--hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \
|
||||||
|
--hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \
|
||||||
|
--hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \
|
||||||
|
--hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \
|
||||||
|
--hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \
|
||||||
|
--hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \
|
||||||
|
--hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \
|
||||||
|
--hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \
|
||||||
|
--hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \
|
||||||
|
--hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \
|
||||||
|
--hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \
|
||||||
|
--hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \
|
||||||
|
--hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \
|
||||||
|
--hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \
|
||||||
|
--hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \
|
||||||
|
--hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \
|
||||||
|
--hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \
|
||||||
|
--hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \
|
||||||
|
--hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
|
||||||
|
--hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \
|
||||||
|
--hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \
|
||||||
|
--hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \
|
||||||
|
--hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \
|
||||||
|
--hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \
|
||||||
|
--hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \
|
||||||
|
--hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \
|
||||||
|
--hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \
|
||||||
|
--hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \
|
||||||
|
--hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \
|
||||||
|
--hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \
|
||||||
|
--hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \
|
||||||
|
--hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \
|
||||||
|
--hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \
|
||||||
|
--hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \
|
||||||
|
--hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \
|
||||||
|
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
|
||||||
|
# via requests
|
||||||
|
idna==3.10 \
|
||||||
|
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
|
||||||
|
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||||
|
# via requests
|
||||||
|
parse==1.20.2 \
|
||||||
|
--hash=sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558 \
|
||||||
|
--hash=sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce
|
||||||
|
# via
|
||||||
|
# behave
|
||||||
|
# parse-type
|
||||||
|
parse-type==0.6.4 \
|
||||||
|
--hash=sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6 \
|
||||||
|
--hash=sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c
|
||||||
|
# via behave
|
||||||
|
pillow==11.1.0 \
|
||||||
|
--hash=sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83 \
|
||||||
|
--hash=sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96 \
|
||||||
|
--hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \
|
||||||
|
--hash=sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a \
|
||||||
|
--hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \
|
||||||
|
--hash=sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f \
|
||||||
|
--hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \
|
||||||
|
--hash=sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c \
|
||||||
|
--hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \
|
||||||
|
--hash=sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49 \
|
||||||
|
--hash=sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91 \
|
||||||
|
--hash=sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0 \
|
||||||
|
--hash=sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2 \
|
||||||
|
--hash=sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5 \
|
||||||
|
--hash=sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884 \
|
||||||
|
--hash=sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e \
|
||||||
|
--hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \
|
||||||
|
--hash=sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196 \
|
||||||
|
--hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \
|
||||||
|
--hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \
|
||||||
|
--hash=sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269 \
|
||||||
|
--hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \
|
||||||
|
--hash=sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb \
|
||||||
|
--hash=sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a \
|
||||||
|
--hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \
|
||||||
|
--hash=sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1 \
|
||||||
|
--hash=sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8 \
|
||||||
|
--hash=sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90 \
|
||||||
|
--hash=sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc \
|
||||||
|
--hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \
|
||||||
|
--hash=sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1 \
|
||||||
|
--hash=sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3 \
|
||||||
|
--hash=sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35 \
|
||||||
|
--hash=sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f \
|
||||||
|
--hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \
|
||||||
|
--hash=sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2 \
|
||||||
|
--hash=sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2 \
|
||||||
|
--hash=sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf \
|
||||||
|
--hash=sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65 \
|
||||||
|
--hash=sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b \
|
||||||
|
--hash=sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442 \
|
||||||
|
--hash=sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2 \
|
||||||
|
--hash=sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade \
|
||||||
|
--hash=sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482 \
|
||||||
|
--hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \
|
||||||
|
--hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \
|
||||||
|
--hash=sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a \
|
||||||
|
--hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \
|
||||||
|
--hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \
|
||||||
|
--hash=sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a \
|
||||||
|
--hash=sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07 \
|
||||||
|
--hash=sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6 \
|
||||||
|
--hash=sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f \
|
||||||
|
--hash=sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e \
|
||||||
|
--hash=sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192 \
|
||||||
|
--hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \
|
||||||
|
--hash=sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6 \
|
||||||
|
--hash=sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73 \
|
||||||
|
--hash=sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f \
|
||||||
|
--hash=sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6 \
|
||||||
|
--hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \
|
||||||
|
--hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \
|
||||||
|
--hash=sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457 \
|
||||||
|
--hash=sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8 \
|
||||||
|
--hash=sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26 \
|
||||||
|
--hash=sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5 \
|
||||||
|
--hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \
|
||||||
|
--hash=sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070 \
|
||||||
|
--hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \
|
||||||
|
--hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \
|
||||||
|
--hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761
|
||||||
|
# via reportlab
|
||||||
|
pycryptodome==3.21.0 \
|
||||||
|
--hash=sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8 \
|
||||||
|
--hash=sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d \
|
||||||
|
--hash=sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0 \
|
||||||
|
--hash=sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93 \
|
||||||
|
--hash=sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4 \
|
||||||
|
--hash=sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a \
|
||||||
|
--hash=sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764 \
|
||||||
|
--hash=sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca \
|
||||||
|
--hash=sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e \
|
||||||
|
--hash=sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e \
|
||||||
|
--hash=sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd \
|
||||||
|
--hash=sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f \
|
||||||
|
--hash=sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6 \
|
||||||
|
--hash=sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb \
|
||||||
|
--hash=sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e \
|
||||||
|
--hash=sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1 \
|
||||||
|
--hash=sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6 \
|
||||||
|
--hash=sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a \
|
||||||
|
--hash=sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c \
|
||||||
|
--hash=sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2 \
|
||||||
|
--hash=sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4 \
|
||||||
|
--hash=sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3 \
|
||||||
|
--hash=sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819 \
|
||||||
|
--hash=sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568 \
|
||||||
|
--hash=sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53 \
|
||||||
|
--hash=sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3 \
|
||||||
|
--hash=sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8 \
|
||||||
|
--hash=sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd \
|
||||||
|
--hash=sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b \
|
||||||
|
--hash=sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b \
|
||||||
|
--hash=sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297 \
|
||||||
|
--hash=sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58
|
||||||
|
# via -r cucumber\requirements.in
|
||||||
|
pypdf2==3.0.1 \
|
||||||
|
--hash=sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440 \
|
||||||
|
--hash=sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928
|
||||||
|
# via -r cucumber\requirements.in
|
||||||
|
reportlab==4.2.5 \
|
||||||
|
--hash=sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f \
|
||||||
|
--hash=sha256:eb2745525a982d9880babb991619e97ac3f661fae30571b7d50387026ca765ee
|
||||||
|
# via -r cucumber\requirements.in
|
||||||
|
requests==2.32.3 \
|
||||||
|
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
|
||||||
|
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||||
|
# via -r cucumber\requirements.in
|
||||||
|
six==1.17.0 \
|
||||||
|
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||||
|
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||||
|
# via
|
||||||
|
# behave
|
||||||
|
# parse-type
|
||||||
|
urllib3==2.3.0 \
|
||||||
|
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
|
||||||
|
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
|
||||||
|
# via requests
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
services:
|
||||||
|
stirling-pdf:
|
||||||
|
container_name: Stirling-PDF-Security-Fat-Postgres
|
||||||
|
image: stirlingtools/stirling-pdf:latest-fat-postgres
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 4G
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'" ]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 16
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
volumes:
|
||||||
|
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||||
|
- ./stirling/latest/config:/configs:rw
|
||||||
|
- ./stirling/latest/logs:/logs:rw
|
||||||
|
environment:
|
||||||
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
PUID: 1002
|
||||||
|
PGID: 1002
|
||||||
|
UMASK: "022"
|
||||||
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
|
UI_APPNAME: Stirling-PDF
|
||||||
|
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security and PostgreSQL
|
||||||
|
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat-PostgreSQL
|
||||||
|
SYSTEM_MAXFILESIZE: "100"
|
||||||
|
METRICS_ENABLED: "true"
|
||||||
|
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||||
|
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
|
||||||
|
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
|
||||||
|
SYSTEM_DATASOURCE_USERNAME: "admin"
|
||||||
|
SYSTEM_DATASOURCE_PASSWORD: "stirling"
|
||||||
|
restart: on-failure:5
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: 'postgres:17.2-alpine'
|
||||||
|
restart: on-failure:5
|
||||||
|
container_name: db
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: "stirling_pdf"
|
||||||
|
POSTGRES_USER: "admin"
|
||||||
|
POSTGRES_PASSWORD: "stirling"
|
||||||
|
shm_size: "512mb"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512m
|
||||||
|
cpus: "0.5"
|
||||||
|
healthcheck:
|
||||||
|
test: [ "CMD-SHELL", "pg_isready -U admin stirling_pdf" ]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
volumes:
|
||||||
|
- ./stirling/latest/data:/pgdata
|
||||||
@@ -14,9 +14,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 8080:8080
|
- 8080:8080
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- ./stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- ./stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||||
- /stirling/latest/config:/configs:rw
|
- ./stirling/latest/config:/configs:rw
|
||||||
- /stirling/latest/logs:/logs:rw
|
- ./stirling/latest/logs:/logs:rw
|
||||||
environment:
|
environment:
|
||||||
DOCKER_ENABLE_SECURITY: "true"
|
DOCKER_ENABLE_SECURITY: "true"
|
||||||
SECURITY_ENABLELOGIN: "true"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
|
|||||||
@@ -247,6 +247,11 @@ ignore = [
|
|||||||
'showJS.tags',
|
'showJS.tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[zh_BO]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[zh_CN]
|
[zh_CN]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
|||||||
@@ -27,4 +27,9 @@ public class EEAppConfig {
|
|||||||
public boolean runningEnterpriseEdition() {
|
public boolean runningEnterpriseEdition() {
|
||||||
return licenseKeyChecker.getEnterpriseEnabledResult();
|
return licenseKeyChecker.getEnterpriseEnabledResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "SSOAutoLogin")
|
||||||
|
public boolean ssoAutoLogin() {
|
||||||
|
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class KeygenLicenseVerifier {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
log.info(" validateLicenseResponse body: " + response.body());
|
log.debug(" validateLicenseResponse body: " + response.body());
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class LicenseKeyChecker {
|
|||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private boolean enterpriseEnbaledResult = false;
|
private boolean enterpriseEnabledResult = false;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public LicenseKeyChecker(
|
public LicenseKeyChecker(
|
||||||
@@ -35,12 +35,12 @@ public class LicenseKeyChecker {
|
|||||||
|
|
||||||
private void checkLicense() {
|
private void checkLicense() {
|
||||||
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||||
enterpriseEnbaledResult = false;
|
enterpriseEnabledResult = false;
|
||||||
} else {
|
} else {
|
||||||
enterpriseEnbaledResult =
|
enterpriseEnabledResult =
|
||||||
licenseService.verifyLicense(
|
licenseService.verifyLicense(
|
||||||
applicationProperties.getEnterpriseEdition().getKey());
|
applicationProperties.getEnterpriseEdition().getKey());
|
||||||
if (enterpriseEnbaledResult) {
|
if (enterpriseEnabledResult) {
|
||||||
log.info("License key is valid.");
|
log.info("License key is valid.");
|
||||||
} else {
|
} else {
|
||||||
log.info("License key is invalid.");
|
log.info("License key is invalid.");
|
||||||
@@ -55,6 +55,6 @@ public class LicenseKeyChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean getEnterpriseEnabledResult() {
|
public boolean getEnterpriseEnabledResult() {
|
||||||
return enterpriseEnbaledResult;
|
return enterpriseEnabledResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@@ -24,15 +25,17 @@ import jakarta.annotation.PreDestroy;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
import stirling.software.SPDF.config.ConfigInitializer;
|
import stirling.software.SPDF.config.ConfigInitializer;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@SpringBootApplication
|
|
||||||
@EnableScheduling
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SPdfApplication {
|
@EnableScheduling
|
||||||
|
@SpringBootApplication
|
||||||
|
public class SPDFApplication {
|
||||||
|
|
||||||
private static String baseUrlStatic;
|
|
||||||
private static String serverPortStatic;
|
private static String serverPortStatic;
|
||||||
|
private static String baseUrlStatic;
|
||||||
|
|
||||||
private final Environment env;
|
private final Environment env;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final WebBrowser webBrowser;
|
private final WebBrowser webBrowser;
|
||||||
@@ -40,7 +43,7 @@ public class SPdfApplication {
|
|||||||
@Value("${baseUrl:http://localhost}")
|
@Value("${baseUrl:http://localhost}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
public SPdfApplication(
|
public SPDFApplication(
|
||||||
Environment env,
|
Environment env,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Autowired(required = false) WebBrowser webBrowser) {
|
@Autowired(required = false) WebBrowser webBrowser) {
|
||||||
@@ -49,42 +52,41 @@ public class SPdfApplication {
|
|||||||
this.webBrowser = webBrowser;
|
this.webBrowser = webBrowser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
|
||||||
private static String findAvailablePort(int startPort) {
|
|
||||||
int port = startPort;
|
|
||||||
while (!isPortAvailable(port)) {
|
|
||||||
port++;
|
|
||||||
}
|
|
||||||
return String.valueOf(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPortAvailable(int port) {
|
|
||||||
try (ServerSocket socket = new ServerSocket(port)) {
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
|
|
||||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
System.setProperty("java.awt.headless", "false");
|
System.setProperty("java.awt.headless", "false");
|
||||||
app.setHeadless(false);
|
app.setHeadless(false);
|
||||||
props.put("java.awt.headless", "false");
|
props.put("java.awt.headless", "false");
|
||||||
props.put("spring.main.web-application-type", "servlet");
|
props.put("spring.main.web-application-type", "servlet");
|
||||||
}
|
}
|
||||||
app.setAdditionalProfiles("default");
|
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.setAdditionalProfiles(getActiveProfile(args));
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
|
||||||
// External config files
|
ConfigInitializer initializer = new ConfigInitializer();
|
||||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
try {
|
||||||
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
initializer.ensureConfigExists();
|
||||||
} else {
|
} catch (IOException | URISyntaxException e) {
|
||||||
log.warn("External configuration file 'configs/settings.yml' does not exist.");
|
log.error("Error initialising configuration", e);
|
||||||
}
|
}
|
||||||
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
|
// External config files
|
||||||
|
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
|
||||||
|
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
|
||||||
|
propertyFiles.put(
|
||||||
|
"spring.config.additional-location",
|
||||||
|
"file:" + InstallationPathConfig.getSettingsPath());
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"External configuration file '{}' does not exist.",
|
||||||
|
InstallationPathConfig.getSettingsPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
if (!existingLocation.isEmpty()) {
|
if (!existingLocation.isEmpty()) {
|
||||||
@@ -92,57 +94,39 @@ public class SPdfApplication {
|
|||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existingLocation + "file:configs/custom_settings.yml");
|
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
|
||||||
} else {
|
} else {
|
||||||
log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
log.warn(
|
||||||
|
"Custom configuration file '{}' does not exist.",
|
||||||
|
InstallationPathConfig.getCustomSettingsPath());
|
||||||
}
|
}
|
||||||
Properties finalProps = new Properties();
|
Properties finalProps = new Properties();
|
||||||
|
|
||||||
if (!propertyFiles.isEmpty()) {
|
if (!propertyFiles.isEmpty()) {
|
||||||
finalProps.putAll(
|
finalProps.putAll(
|
||||||
Collections.singletonMap(
|
Collections.singletonMap(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
propertyFiles.get("spring.config.additional-location")));
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isEmpty()) {
|
if (!props.isEmpty()) {
|
||||||
finalProps.putAll(props);
|
finalProps.putAll(props);
|
||||||
}
|
}
|
||||||
app.setDefaultProperties(finalProps);
|
app.setDefaultProperties(finalProps);
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
// Ensure directories are created
|
// Ensure directories are created
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(Path.of("customFiles/static/"));
|
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
||||||
Files.createDirectories(Path.of("customFiles/templates/"));
|
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error creating directories: {}", e.getMessage());
|
log.error("Error creating directories: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
printStartupLogs();
|
printStartupLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void printStartupLogs() {
|
|
||||||
log.info("Stirling-PDF Started.");
|
|
||||||
String url = baseUrlStatic + ":" + getStaticPort();
|
|
||||||
log.info("Navigate to {}", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticBaseUrl() {
|
|
||||||
return baseUrlStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticPort() {
|
|
||||||
return serverPortStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value("${server.port:8080}")
|
|
||||||
public void setServerPortStatic(String port) {
|
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
|
||||||
SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port
|
|
||||||
"0";
|
|
||||||
} else {
|
|
||||||
SPdfApplication.serverPortStatic = port;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
baseUrlStatic = this.baseUrl;
|
baseUrlStatic = this.baseUrl;
|
||||||
@@ -173,6 +157,17 @@ public class SPdfApplication {
|
|||||||
log.info("Running configs {}", applicationProperties.toString());
|
log.info("Running configs {}", applicationProperties.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Value("${server.port:8080}")
|
||||||
|
public void setServerPortStatic(String port) {
|
||||||
|
if ("auto".equalsIgnoreCase(port)) {
|
||||||
|
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||||
|
SPDFApplication.serverPortStatic =
|
||||||
|
"0"; // This will let Spring Boot assign an available port
|
||||||
|
} else {
|
||||||
|
SPDFApplication.serverPortStatic = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
if (webBrowser != null) {
|
if (webBrowser != null) {
|
||||||
@@ -180,10 +175,55 @@ public class SPdfApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void printStartupLogs() {
|
||||||
|
log.info("Stirling-PDF Started.");
|
||||||
|
String url = baseUrlStatic + ":" + getStaticPort();
|
||||||
|
log.info("Navigate to {}", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String[] getActiveProfile(String[] args) {
|
||||||
|
if (args == null) {
|
||||||
|
return new String[] {"default"};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String arg : args) {
|
||||||
|
if (arg.contains("spring.profiles.active")) {
|
||||||
|
return arg.substring(args[0].indexOf('=') + 1).split(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[] {"default"};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPortAvailable(int port) {
|
||||||
|
try (ServerSocket socket = new ServerSocket(port)) {
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
||||||
|
private static String findAvailablePort(int startPort) {
|
||||||
|
int port = startPort;
|
||||||
|
while (!isPortAvailable(port)) {
|
||||||
|
port++;
|
||||||
|
}
|
||||||
|
return String.valueOf(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getStaticBaseUrl() {
|
||||||
|
return baseUrlStatic;
|
||||||
|
}
|
||||||
|
|
||||||
public String getNonStaticBaseUrl() {
|
public String getNonStaticBaseUrl() {
|
||||||
return baseUrlStatic;
|
return baseUrlStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getStaticPort() {
|
||||||
|
return serverPortStatic;
|
||||||
|
}
|
||||||
|
|
||||||
public String getNonStaticPort() {
|
public String getNonStaticPort() {
|
||||||
return serverPortStatic;
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,7 @@ import me.friwi.jcefmaven.EnumProgress;
|
|||||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||||
import stirling.software.SPDF.UI.WebBrowser;
|
import stirling.software.SPDF.UI.WebBrowser;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -72,7 +73,8 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
CefAppBuilder builder = new CefAppBuilder();
|
CefAppBuilder builder = new CefAppBuilder();
|
||||||
configureCefSettings(builder);
|
configureCefSettings(builder);
|
||||||
builder.setProgressHandler(createProgressHandler());
|
builder.setProgressHandler(createProgressHandler());
|
||||||
|
builder.setInstallDir(
|
||||||
|
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||||
// Build and initialize CEF
|
// Build and initialize CEF
|
||||||
cefApp = builder.build();
|
cefApp = builder.build();
|
||||||
client = cefApp.createClient();
|
client = cefApp.createClient();
|
||||||
@@ -99,8 +101,16 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
|
|
||||||
private void configureCefSettings(CefAppBuilder builder) {
|
private void configureCefSettings(CefAppBuilder builder) {
|
||||||
CefSettings settings = builder.getCefSettings();
|
CefSettings settings = builder.getCefSettings();
|
||||||
settings.cache_path = new File("jcef-bundle").getAbsolutePath();
|
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||||
settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
|
log.info("basePath " + basePath);
|
||||||
|
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||||
|
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||||
|
// settings.browser_subprocess_path = new File(basePath +
|
||||||
|
// "subprocess").getAbsolutePath();
|
||||||
|
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
||||||
|
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
||||||
|
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
||||||
|
|
||||||
settings.persist_session_cookies = true;
|
settings.persist_session_cookies = true;
|
||||||
settings.windowless_rendering_enabled = false;
|
settings.windowless_rendering_enabled = false;
|
||||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||||
@@ -212,6 +222,9 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupLoadHandler() {
|
private void setupLoadHandler() {
|
||||||
|
final long initStartTime = System.currentTimeMillis();
|
||||||
|
log.info("Setting up load handler at: {}", initStartTime);
|
||||||
|
|
||||||
client.addLoadHandler(
|
client.addLoadHandler(
|
||||||
new CefLoadHandlerAdapter() {
|
new CefLoadHandlerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
@@ -220,32 +233,77 @@ public class DesktopBrowser implements WebBrowser {
|
|||||||
boolean isLoading,
|
boolean isLoading,
|
||||||
boolean canGoBack,
|
boolean canGoBack,
|
||||||
boolean canGoForward) {
|
boolean canGoForward) {
|
||||||
|
log.debug(
|
||||||
|
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
|
||||||
|
+ "browserInitialized: {}, Time elapsed: {}ms",
|
||||||
|
isLoading,
|
||||||
|
canGoBack,
|
||||||
|
canGoForward,
|
||||||
|
browserInitialized,
|
||||||
|
System.currentTimeMillis() - initStartTime);
|
||||||
|
|
||||||
if (!isLoading && !browserInitialized) {
|
if (!isLoading && !browserInitialized) {
|
||||||
|
log.info(
|
||||||
|
"Browser finished loading, preparing to initialize UI components");
|
||||||
browserInitialized = true;
|
browserInitialized = true;
|
||||||
SwingUtilities.invokeLater(
|
SwingUtilities.invokeLater(
|
||||||
() -> {
|
() -> {
|
||||||
|
try {
|
||||||
if (loadingWindow != null) {
|
if (loadingWindow != null) {
|
||||||
Timer timer =
|
log.info("Starting UI initialization sequence");
|
||||||
new Timer(
|
|
||||||
500,
|
// Close loading window first
|
||||||
e -> {
|
loadingWindow.setVisible(false);
|
||||||
loadingWindow.dispose();
|
loadingWindow.dispose();
|
||||||
loadingWindow = null;
|
loadingWindow = null;
|
||||||
|
log.info("Loading window disposed");
|
||||||
|
|
||||||
|
// Then setup the main frame
|
||||||
|
frame.setVisible(false);
|
||||||
frame.dispose();
|
frame.dispose();
|
||||||
frame.setOpacity(1.0f);
|
frame.setOpacity(1.0f);
|
||||||
frame.setUndecorated(false);
|
frame.setUndecorated(false);
|
||||||
frame.pack();
|
frame.pack();
|
||||||
frame.setSize(1280, 800);
|
frame.setSize(1280, 800);
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
|
log.debug("Frame reconfigured");
|
||||||
|
|
||||||
|
// Show the main frame
|
||||||
frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
frame.requestFocus();
|
frame.requestFocus();
|
||||||
frame.toFront();
|
frame.toFront();
|
||||||
|
log.info("Main frame displayed and focused");
|
||||||
|
|
||||||
|
// Focus the browser component
|
||||||
|
Timer focusTimer =
|
||||||
|
new Timer(
|
||||||
|
100,
|
||||||
|
e -> {
|
||||||
|
try {
|
||||||
browser.getUIComponent()
|
browser.getUIComponent()
|
||||||
.requestFocus();
|
.requestFocus();
|
||||||
|
log.info(
|
||||||
|
"Browser component focused");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error(
|
||||||
|
"Error focusing browser",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
timer.setRepeats(false);
|
focusTimer.setRepeats(false);
|
||||||
timer.start();
|
focusTimer.start();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error during UI initialization", e);
|
||||||
|
// Attempt cleanup on error
|
||||||
|
if (loadingWindow != null) {
|
||||||
|
loadingWindow.dispose();
|
||||||
|
loadingWindow = null;
|
||||||
|
}
|
||||||
|
if (frame != null) {
|
||||||
|
frame.setVisible(true);
|
||||||
|
frame.requestFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,12 @@ public class LoadingWindow extends JDialog {
|
|||||||
private final JLabel statusLabel;
|
private final JLabel statusLabel;
|
||||||
private final JPanel mainPanel;
|
private final JPanel mainPanel;
|
||||||
private final JLabel brandLabel;
|
private final JLabel brandLabel;
|
||||||
|
private long startTime;
|
||||||
|
|
||||||
public LoadingWindow(Frame parent, String initialUrl) {
|
public LoadingWindow(Frame parent, String initialUrl) {
|
||||||
super(parent, "Initializing Stirling-PDF", true);
|
super(parent, "Initializing Stirling-PDF", true);
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
mainPanel = new JPanel();
|
mainPanel = new JPanel();
|
||||||
@@ -29,8 +32,8 @@ public class LoadingWindow extends JDialog {
|
|||||||
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||||
gbc.insets = new Insets(5, 5, 5, 5);
|
gbc.insets = new Insets(5, 5, 5, 5);
|
||||||
gbc.weightx = 1.0; // Add horizontal weight
|
gbc.weightx = 1.0;
|
||||||
gbc.weighty = 0.0; // Add vertical weight
|
gbc.weighty = 0.0;
|
||||||
|
|
||||||
// Add icon
|
// Add icon
|
||||||
try {
|
try {
|
||||||
@@ -43,12 +46,14 @@ public class LoadingWindow extends JDialog {
|
|||||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
gbc.gridy = 0;
|
gbc.gridy = 0;
|
||||||
mainPanel.add(iconLabel, gbc);
|
mainPanel.add(iconLabel, gbc);
|
||||||
|
log.debug("Icon loaded and scaled successfully");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to load icon", e);
|
log.error("Failed to load icon", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL Label with explicit size
|
// URL Label with explicit size
|
||||||
brandLabel = new JLabel(initialUrl);
|
brandLabel = new JLabel(initialUrl);
|
||||||
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||||
@@ -63,6 +68,7 @@ public class LoadingWindow extends JDialog {
|
|||||||
statusLabel.setPreferredSize(new Dimension(300, 25));
|
statusLabel.setPreferredSize(new Dimension(300, 25));
|
||||||
gbc.gridy = 2;
|
gbc.gridy = 2;
|
||||||
mainPanel.add(statusLabel, gbc);
|
mainPanel.add(statusLabel, gbc);
|
||||||
|
|
||||||
// Progress bar with explicit size
|
// Progress bar with explicit size
|
||||||
progressBar = new JProgressBar(0, 100);
|
progressBar = new JProgressBar(0, 100);
|
||||||
progressBar.setStringPainted(true);
|
progressBar.setStringPainted(true);
|
||||||
@@ -82,33 +88,78 @@ public class LoadingWindow extends JDialog {
|
|||||||
setAlwaysOnTop(true);
|
setAlwaysOnTop(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setStatus("Starting...");
|
setStatus("Starting...");
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"LoadingWindow initialization completed in {}ms",
|
||||||
|
System.currentTimeMillis() - startTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgress(final int progress) {
|
public void setProgress(final int progress) {
|
||||||
SwingUtilities.invokeLater(
|
SwingUtilities.invokeLater(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
progressBar.setValue(Math.min(Math.max(progress, 0), 100));
|
int validProgress = Math.min(Math.max(progress, 0), 100);
|
||||||
progressBar.setString(progress + "%");
|
log.info(
|
||||||
|
"Setting progress to {}% at {}ms since start",
|
||||||
|
validProgress, System.currentTimeMillis() - startTime);
|
||||||
|
|
||||||
|
// Log additional details when near 90%
|
||||||
|
if (validProgress >= 85 && validProgress <= 95) {
|
||||||
|
log.info(
|
||||||
|
"Near 90% progress - Current status: {}, Window visible: {}, "
|
||||||
|
+ "Progress bar responding: {}, Memory usage: {}MB",
|
||||||
|
statusLabel.getText(),
|
||||||
|
isVisible(),
|
||||||
|
progressBar.isEnabled(),
|
||||||
|
Runtime.getRuntime().totalMemory() / (1024 * 1024));
|
||||||
|
|
||||||
|
// Add thread state logging
|
||||||
|
Thread currentThread = Thread.currentThread();
|
||||||
|
log.debug(
|
||||||
|
"Current thread state - Name: {}, State: {}, Priority: {}",
|
||||||
|
currentThread.getName(),
|
||||||
|
currentThread.getState(),
|
||||||
|
currentThread.getPriority());
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.setValue(validProgress);
|
||||||
|
progressBar.setString(validProgress + "%");
|
||||||
mainPanel.revalidate();
|
mainPanel.revalidate();
|
||||||
mainPanel.repaint();
|
mainPanel.repaint();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating progress", e);
|
log.error("Error updating progress to " + progress, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStatus(final String status) {
|
public void setStatus(final String status) {
|
||||||
log.info(status);
|
log.info(
|
||||||
|
"Status update at {}ms - Setting status to: {}",
|
||||||
|
System.currentTimeMillis() - startTime,
|
||||||
|
status);
|
||||||
|
|
||||||
SwingUtilities.invokeLater(
|
SwingUtilities.invokeLater(
|
||||||
() -> {
|
() -> {
|
||||||
try {
|
try {
|
||||||
statusLabel.setText(status != null ? status : "");
|
String validStatus = status != null ? status : "";
|
||||||
|
statusLabel.setText(validStatus);
|
||||||
|
|
||||||
|
// Log UI state when status changes
|
||||||
|
log.debug(
|
||||||
|
"UI State - Window visible: {}, Progress: {}%, Status: {}",
|
||||||
|
isVisible(), progressBar.getValue(), validStatus);
|
||||||
|
|
||||||
mainPanel.revalidate();
|
mainPanel.revalidate();
|
||||||
mainPanel.repaint();
|
mainPanel.repaint();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error updating status", e);
|
log.error("Error updating status to: " + status, e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,16 +136,6 @@ public class AppConfig {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "watchedFoldersDir")
|
|
||||||
public String watchedFoldersDir() {
|
|
||||||
return "./pipeline/watchedFolders/";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "finishedFoldersDir")
|
|
||||||
public String finishedFoldersDir() {
|
|
||||||
return "./pipeline/finishedFolders/";
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean(name = "directoryFilter")
|
@Bean(name = "directoryFilter")
|
||||||
public Predicate<Path> processOnlyFiles() {
|
public Predicate<Path> processOnlyFiles() {
|
||||||
return path -> {
|
return path -> {
|
||||||
|
|||||||
@@ -16,27 +16,15 @@ 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.SimpleYamlImplementation;
|
||||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ConfigInitializer
|
public class ConfigInitializer {
|
||||||
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
|
||||||
try {
|
|
||||||
ensureConfigExists();
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("Failed to initialize application configuration", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||||
// Define the path to the external config directory
|
// Define the path to the external config directory
|
||||||
Path destPath = Paths.get("configs", "settings.yml");
|
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
|
|
||||||
// Check if the file already exists
|
// Check if the file already exists
|
||||||
if (Files.notExists(destPath)) {
|
if (Files.notExists(destPath)) {
|
||||||
@@ -53,10 +41,11 @@ public class ConfigInitializer
|
|||||||
"Resource file not found: settings.yml.template");
|
"Resource file not found: settings.yml.template");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("Created settings file from template");
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Define the path to the config settings file
|
// Define the path to the config settings file
|
||||||
Path settingsPath = Paths.get("configs", "settings.yml");
|
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
// Load the template resource
|
// Load the template resource
|
||||||
URL settingsTemplateResource =
|
URL settingsTemplateResource =
|
||||||
getClass().getClassLoader().getResource("settings.yml.template");
|
getClass().getClassLoader().getResource("settings.yml.template");
|
||||||
@@ -120,7 +109,7 @@ public class ConfigInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create custom settings file if it doesn't exist
|
// Create custom settings file if it doesn't exist
|
||||||
Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
|
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||||
if (!Files.exists(customSettingsPath)) {
|
if (!Files.exists(customSettingsPath)) {
|
||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -135,6 +136,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Security", "remove-cert-sign");
|
addEndpointToGroup("Security", "remove-cert-sign");
|
||||||
addEndpointToGroup("Security", "sanitize-pdf");
|
addEndpointToGroup("Security", "sanitize-pdf");
|
||||||
addEndpointToGroup("Security", "auto-redact");
|
addEndpointToGroup("Security", "auto-redact");
|
||||||
|
addEndpointToGroup("Security", "redact");
|
||||||
|
|
||||||
// Adding endpoints to "Other" group
|
// Adding endpoints to "Other" group
|
||||||
addEndpointToGroup("Other", "ocr-pdf");
|
addEndpointToGroup("Other", "ocr-pdf");
|
||||||
@@ -234,6 +236,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||||
addEndpointToGroup("Java", "show-javascript");
|
addEndpointToGroup("Java", "show-javascript");
|
||||||
addEndpointToGroup("Java", "auto-redact");
|
addEndpointToGroup("Java", "auto-redact");
|
||||||
|
addEndpointToGroup("Java", "redact");
|
||||||
addEndpointToGroup("Java", "pdf-to-csv");
|
addEndpointToGroup("Java", "pdf-to-csv");
|
||||||
addEndpointToGroup("Java", "split-by-size-or-count");
|
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||||
addEndpointToGroup("Java", "overlay-pdf");
|
addEndpointToGroup("Java", "overlay-pdf");
|
||||||
@@ -265,9 +268,14 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
|
if (applicationProperties != null && applicationProperties.getEndpoints() != null) {
|
||||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||||
|
|
||||||
if (!bookAndHtmlFormatsInstalled) {
|
if (!bookAndHtmlFormatsInstalled) {
|
||||||
|
if (groupsToRemove == null) {
|
||||||
|
groupsToRemove = new ArrayList<>();
|
||||||
|
}
|
||||||
groupsToRemove.add("Calibre");
|
groupsToRemove.add("Calibre");
|
||||||
}
|
}
|
||||||
if (endpointsToRemove != null) {
|
if (endpointsToRemove != null) {
|
||||||
@@ -282,6 +290,7 @@ public class EndpointConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Set<String> getEndpointsForGroup(String group) {
|
public Set<String> getEndpointsForGroup(String group) {
|
||||||
return endpointGroups.getOrDefault(group, new HashSet<>());
|
return endpointGroups.getOrDefault(group, new HashSet<>());
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
|||||||
String characterEncoding,
|
String characterEncoding,
|
||||||
Map<String, Object> templateResolutionAttributes) {
|
Map<String, Object> templateResolutionAttributes) {
|
||||||
Resource resource =
|
Resource resource =
|
||||||
resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
|
resourceLoader.getResource(
|
||||||
|
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
|
||||||
try {
|
try {
|
||||||
if (resource.exists() && resource.isReadable()) {
|
if (resource.exists() && resource.isReadable()) {
|
||||||
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InstallationPathConfig {
|
||||||
|
private static final String BASE_PATH;
|
||||||
|
|
||||||
|
// Root paths
|
||||||
|
private static final String LOG_PATH;
|
||||||
|
private static final String CONFIG_PATH;
|
||||||
|
private static final String PIPELINE_PATH;
|
||||||
|
private static final String CUSTOM_FILES_PATH;
|
||||||
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
|
|
||||||
|
// Config paths
|
||||||
|
private static final String SETTINGS_PATH;
|
||||||
|
private static final String CUSTOM_SETTINGS_PATH;
|
||||||
|
|
||||||
|
// Pipeline paths
|
||||||
|
private static final String PIPELINE_WATCHED_FOLDERS_PATH;
|
||||||
|
private static final String PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
|
|
||||||
|
// Custom file paths
|
||||||
|
private static final String STATIC_PATH;
|
||||||
|
private static final String TEMPLATES_PATH;
|
||||||
|
private static final String SIGNATURES_PATH;
|
||||||
|
|
||||||
|
static {
|
||||||
|
BASE_PATH = initializeBasePath();
|
||||||
|
|
||||||
|
// Initialize root paths
|
||||||
|
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
||||||
|
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
||||||
|
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
|
||||||
|
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
||||||
|
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
||||||
|
|
||||||
|
// Initialize config paths
|
||||||
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
|
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
|
||||||
|
|
||||||
|
// Initialize pipeline paths
|
||||||
|
PIPELINE_WATCHED_FOLDERS_PATH = PIPELINE_PATH + "watchedFolders" + File.separator;
|
||||||
|
PIPELINE_FINISHED_FOLDERS_PATH = PIPELINE_PATH + "finishedFolders" + File.separator;
|
||||||
|
|
||||||
|
// Initialize custom file paths
|
||||||
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
|
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||||
|
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String initializeBasePath() {
|
||||||
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
|
if (os.contains("win")) {
|
||||||
|
return System.getenv("APPDATA") + File.separator + "Stirling-PDF" + File.separator;
|
||||||
|
} else if (os.contains("mac")) {
|
||||||
|
return System.getProperty("user.home")
|
||||||
|
+ File.separator
|
||||||
|
+ "Library"
|
||||||
|
+ File.separator
|
||||||
|
+ "Application Support"
|
||||||
|
+ File.separator
|
||||||
|
+ "Stirling-PDF"
|
||||||
|
+ File.separator;
|
||||||
|
} else {
|
||||||
|
return System.getProperty("user.home")
|
||||||
|
+ File.separator
|
||||||
|
+ ".config"
|
||||||
|
+ File.separator
|
||||||
|
+ "Stirling-PDF"
|
||||||
|
+ File.separator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "./";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPath() {
|
||||||
|
return BASE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getLogPath() {
|
||||||
|
return LOG_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getConfigPath() {
|
||||||
|
return CONFIG_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPipelinePath() {
|
||||||
|
return PIPELINE_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCustomFilesPath() {
|
||||||
|
return CUSTOM_FILES_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getClientWebUIPath() {
|
||||||
|
return CLIENT_WEBUI_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSettingsPath() {
|
||||||
|
return SETTINGS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCustomSettingsPath() {
|
||||||
|
return CUSTOM_SETTINGS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPipelineWatchedFoldersDir() {
|
||||||
|
return PIPELINE_WATCHED_FOLDERS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPipelineFinishedFoldersDir() {
|
||||||
|
return PIPELINE_FINISHED_FOLDERS_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getStaticPath() {
|
||||||
|
return STATIC_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTemplatesPath() {
|
||||||
|
return TEMPLATES_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getSignaturesPath() {
|
||||||
|
return SIGNATURES_PATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import ch.qos.logback.core.PropertyDefinerBase;
|
||||||
|
|
||||||
|
public class LogbackPropertyLoader extends PropertyDefinerBase {
|
||||||
|
@Override
|
||||||
|
public String getPropertyValue() {
|
||||||
|
return InstallationPathConfig.getLogPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
// Handler for external static resources
|
// Handler for external static resources
|
||||||
registry.addResourceHandler("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
.addResourceLocations(
|
||||||
|
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||||
// .setCachePeriod(0); // Optional: disable caching
|
// .setCachePeriod(0); // Optional: disable caching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
factory.setResources(encodedResource.getResource());
|
factory.setResources(encodedResource.getResource());
|
||||||
|
|
||||||
Properties properties = factory.getObject();
|
Properties properties = factory.getObject();
|
||||||
|
|
||||||
return new PropertiesPropertySource(
|
return new PropertiesPropertySource(
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.interfaces;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
|
||||||
|
|
||||||
public interface DatabaseBackupInterface {
|
|
||||||
|
|
||||||
void exportDatabase() throws IOException;
|
|
||||||
|
|
||||||
boolean importDatabase();
|
|
||||||
|
|
||||||
boolean hasBackup();
|
|
||||||
|
|
||||||
List<FileInfo> getBackupList();
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package stirling.software.SPDF.config.interfaces;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
|
public interface DatabaseInterface {
|
||||||
|
void exportDatabase() throws SQLException, UnsupportedProviderException;
|
||||||
|
|
||||||
|
void importDatabase();
|
||||||
|
|
||||||
|
boolean hasBackup();
|
||||||
|
|
||||||
|
List<FileInfo> getBackupList();
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.SPdfApplication;
|
import stirling.software.SPDF.SPDFApplication;
|
||||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
@@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
|
|
||||||
// Construct URLs required for SAML configuration
|
// Construct URLs required for SAML configuration
|
||||||
String serverUrl =
|
String serverUrl =
|
||||||
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
|
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||||
|
|
||||||
String relyingPartyIdentifier =
|
String relyingPartyIdentifier =
|
||||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||||
|
|||||||
@@ -1,49 +1,56 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.sql.SQLException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Component
|
||||||
public class InitialSecuritySetup {
|
public class InitialSecuritySetup {
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private final DatabaseBackupInterface databaseBackupHelper;
|
private final DatabaseInterface databaseService;
|
||||||
|
|
||||||
public InitialSecuritySetup(
|
public InitialSecuritySetup(
|
||||||
UserService userService,
|
UserService userService,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
DatabaseBackupInterface databaseBackupHelper) {
|
DatabaseInterface databaseService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.databaseBackupHelper = databaseBackupHelper;
|
this.databaseService = databaseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() throws IllegalArgumentException, IOException {
|
public void init() {
|
||||||
if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
|
try {
|
||||||
databaseBackupHelper.importDatabase();
|
if (databaseService.hasBackup()) {
|
||||||
} else if (!userService.hasUsers()) {
|
databaseService.importDatabase();
|
||||||
initializeAdminUser();
|
|
||||||
} else {
|
|
||||||
databaseBackupHelper.exportDatabase();
|
|
||||||
userService.migrateOauth2ToSSO();
|
|
||||||
}
|
|
||||||
initializeInternalApiUser();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeAdminUser() throws IOException {
|
if (!userService.hasUsers()) {
|
||||||
|
initializeAdminUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
userService.migrateOauth2ToSSO();
|
||||||
|
initializeInternalApiUser();
|
||||||
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
|
log.error("Failed to initialize security setup.", e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
|
||||||
String initialUsername =
|
String initialUsername =
|
||||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
String initialPassword =
|
String initialPassword =
|
||||||
@@ -52,36 +59,34 @@ public class InitialSecuritySetup {
|
|||||||
&& !initialUsername.isEmpty()
|
&& !initialUsername.isEmpty()
|
||||||
&& initialPassword != null
|
&& initialPassword != null
|
||||||
&& !initialPassword.isEmpty()
|
&& !initialPassword.isEmpty()
|
||||||
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) {
|
||||||
try {
|
|
||||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||||
log.info("Admin user created: " + initialUsername);
|
log.info("Admin user created: {}", initialUsername);
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
log.error("Failed to initialize security setup", e);
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
createDefaultAdminUser();
|
createDefaultAdminUser();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
|
private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException {
|
||||||
String defaultUsername = "admin";
|
String defaultUsername = "admin";
|
||||||
String defaultPassword = "stirling";
|
String defaultPassword = "stirling";
|
||||||
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
|
||||||
|
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
|
||||||
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
|
||||||
log.info("Default admin user created: " + defaultUsername);
|
log.info("Default admin user created: {}", defaultUsername);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
|
private void initializeInternalApiUser()
|
||||||
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
|
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
|
||||||
userService.saveUser(
|
userService.saveUser(
|
||||||
Role.INTERNAL_API_USER.getRoleId(),
|
Role.INTERNAL_API_USER.getRoleId(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
Role.INTERNAL_API_USER.getRoleId());
|
Role.INTERNAL_API_USER.getRoleId());
|
||||||
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId());
|
||||||
}
|
}
|
||||||
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
|
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,24 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
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.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
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.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
||||||
import org.springframework.security.oauth2.client.registration.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.saml2.core.Saml2X509Credential;
|
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||||
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;
|
||||||
@@ -43,24 +28,16 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
|||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
|
||||||
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
import stirling.software.SPDF.repository.PersistentLoginRepository;
|
import stirling.software.SPDF.repository.PersistentLoginRepository;
|
||||||
|
|
||||||
@@ -72,7 +49,7 @@ import stirling.software.SPDF.repository.PersistentLoginRepository;
|
|||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
private final CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
@Lazy private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
@@ -86,16 +63,10 @@ public class SecurityConfiguration {
|
|||||||
private final FirstLoginFilter firstLoginFilter;
|
private final FirstLoginFilter firstLoginFilter;
|
||||||
private final SessionPersistentRegistry sessionRegistry;
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
private final PersistentLoginRepository persistentLoginRepository;
|
private final PersistentLoginRepository persistentLoginRepository;
|
||||||
|
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
|
||||||
|
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
|
||||||
|
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
|
||||||
|
|
||||||
// // Only Dev test
|
|
||||||
// @Bean
|
|
||||||
// public WebSecurityCustomizer webSecurityCustomizer() {
|
|
||||||
// return (web) ->
|
|
||||||
// web.ignoring()
|
|
||||||
// .requestMatchers(
|
|
||||||
// "/css/**", "/images/**", "/js/**", "/**.svg",
|
|
||||||
// "/pdfjs-legacy/**");
|
|
||||||
// }
|
|
||||||
public SecurityConfiguration(
|
public SecurityConfiguration(
|
||||||
PersistentLoginRepository persistentLoginRepository,
|
PersistentLoginRepository persistentLoginRepository,
|
||||||
CustomUserDetailsService userDetailsService,
|
CustomUserDetailsService userDetailsService,
|
||||||
@@ -106,7 +77,12 @@ public class SecurityConfiguration {
|
|||||||
UserAuthenticationFilter userAuthenticationFilter,
|
UserAuthenticationFilter userAuthenticationFilter,
|
||||||
LoginAttemptService loginAttemptService,
|
LoginAttemptService loginAttemptService,
|
||||||
FirstLoginFilter firstLoginFilter,
|
FirstLoginFilter firstLoginFilter,
|
||||||
SessionPersistentRegistry sessionRegistry) {
|
SessionPersistentRegistry sessionRegistry,
|
||||||
|
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
|
||||||
|
@Autowired(required = false)
|
||||||
|
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
|
||||||
|
@Autowired(required = false)
|
||||||
|
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
@@ -117,6 +93,9 @@ public class SecurityConfiguration {
|
|||||||
this.firstLoginFilter = firstLoginFilter;
|
this.firstLoginFilter = firstLoginFilter;
|
||||||
this.sessionRegistry = sessionRegistry;
|
this.sessionRegistry = sessionRegistry;
|
||||||
this.persistentLoginRepository = persistentLoginRepository;
|
this.persistentLoginRepository = persistentLoginRepository;
|
||||||
|
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||||
|
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||||
|
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -274,7 +253,7 @@ public class SecurityConfiguration {
|
|||||||
userService,
|
userService,
|
||||||
loginAttemptService))
|
loginAttemptService))
|
||||||
.userAuthoritiesMapper(
|
.userAuthoritiesMapper(
|
||||||
userAuthoritiesMapper()))
|
oAuth2userAuthoritiesMapper))
|
||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
@@ -291,7 +270,7 @@ public class SecurityConfiguration {
|
|||||||
try {
|
try {
|
||||||
saml2.loginPage("/saml2")
|
saml2.loginPage("/saml2")
|
||||||
.relyingPartyRegistrationRepository(
|
.relyingPartyRegistrationRepository(
|
||||||
relyingPartyRegistrations())
|
saml2RelyingPartyRegistrations)
|
||||||
.authenticationManager(
|
.authenticationManager(
|
||||||
new ProviderManager(authenticationProvider))
|
new ProviderManager(authenticationProvider))
|
||||||
.successHandler(
|
.successHandler(
|
||||||
@@ -302,8 +281,7 @@ public class SecurityConfiguration {
|
|||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
.authenticationRequestResolver(
|
.authenticationRequestResolver(
|
||||||
authenticationRequestResolver(
|
saml2AuthenticationRequestResolver);
|
||||||
relyingPartyRegistrations()));
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error configuring SAML2 login", e);
|
log.error("Error configuring SAML2 login", e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
@@ -311,244 +289,11 @@ public class SecurityConfiguration {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
|
||||||
// CookieCsrfTokenRepository cookieRepo =
|
|
||||||
// CookieCsrfTokenRepository.withHttpOnlyFalse();
|
|
||||||
// CsrfTokenRequestAttributeHandler requestHandler =
|
|
||||||
// new CsrfTokenRequestAttributeHandler();
|
|
||||||
// requestHandler.setCsrfRequestAttributeName(null);
|
|
||||||
// http.csrf(
|
|
||||||
// csrf ->
|
|
||||||
// csrf.csrfTokenRepository(cookieRepo)
|
|
||||||
// .csrfTokenRequestHandler(requestHandler));
|
|
||||||
// }
|
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
}
|
}
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
|
||||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
|
||||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
|
||||||
Resource certificateResource = samlConf.getSpCert();
|
|
||||||
Saml2X509Credential signingCredential =
|
|
||||||
new Saml2X509Credential(
|
|
||||||
CertificateUtils.readPrivateKey(privateKeyResource),
|
|
||||||
CertificateUtils.readCertificate(certificateResource),
|
|
||||||
Saml2X509CredentialType.SIGNING);
|
|
||||||
RelyingPartyRegistration rp =
|
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
|
||||||
.signingX509Credentials(c -> c.add(signingCredential))
|
|
||||||
.assertingPartyMetadata(
|
|
||||||
metadata ->
|
|
||||||
metadata.entityId(samlConf.getIdpIssuer())
|
|
||||||
.singleSignOnServiceLocation(
|
|
||||||
samlConf.getIdpSingleLoginUrl())
|
|
||||||
.verificationX509Credentials(
|
|
||||||
c -> c.add(verificationCredential))
|
|
||||||
.singleSignOnServiceBinding(
|
|
||||||
Saml2MessageBinding.POST)
|
|
||||||
.wantAuthnRequestsSigned(true))
|
|
||||||
.build();
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
@ConditionalOnProperty(
|
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
|
||||||
OpenSaml4AuthenticationRequestResolver resolver =
|
|
||||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
|
||||||
resolver.setAuthnRequestCustomizer(
|
|
||||||
customizer -> {
|
|
||||||
log.debug("Customizing SAML Authentication request");
|
|
||||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
|
||||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
|
||||||
if (authnRequest.getID() == null) {
|
|
||||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
|
||||||
}
|
|
||||||
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
|
||||||
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
|
||||||
log.debug(
|
|
||||||
"AuthnRequest Issuer: {}",
|
|
||||||
authnRequest.getIssuer() != null
|
|
||||||
? authnRequest.getIssuer().getValue()
|
|
||||||
: "null");
|
|
||||||
HttpServletRequest request = customizer.getRequest();
|
|
||||||
// Log HTTP request details
|
|
||||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
|
||||||
log.debug("Request URI: {}", request.getRequestURI());
|
|
||||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
|
||||||
log.debug("Query String: {}", request.getQueryString());
|
|
||||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
|
||||||
// Log headers
|
|
||||||
Collections.list(request.getHeaderNames())
|
|
||||||
.forEach(
|
|
||||||
headerName -> {
|
|
||||||
log.debug(
|
|
||||||
"Header - {}: {}",
|
|
||||||
headerName,
|
|
||||||
request.getHeader(headerName));
|
|
||||||
});
|
|
||||||
// Log SAML specific parameters
|
|
||||||
log.debug("SAML Request Parameters:");
|
|
||||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
|
||||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
|
||||||
// Log session debugrmation if exists
|
|
||||||
if (request.getSession(false) != null) {
|
|
||||||
log.debug("Session ID: {}", request.getSession().getId());
|
|
||||||
}
|
|
||||||
// Log any assertions consumer service details if present
|
|
||||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
|
||||||
log.debug(
|
|
||||||
"AssertionConsumerServiceURL: {}",
|
|
||||||
authnRequest.getAssertionConsumerServiceURL());
|
|
||||||
}
|
|
||||||
// Log NameID policy if present
|
|
||||||
if (authnRequest.getNameIDPolicy() != null) {
|
|
||||||
log.debug(
|
|
||||||
"NameIDPolicy Format: {}",
|
|
||||||
authnRequest.getNameIDPolicy().getFormat());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return resolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
provider.setUserDetailsService(userDetailsService);
|
provider.setUserDetailsService(userDetailsService);
|
||||||
@@ -556,46 +301,6 @@ public class SecurityConfiguration {
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
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() {
|
||||||
// Example limit TODO add config level
|
// Example limit TODO add config level
|
||||||
|
|||||||
@@ -22,22 +22,31 @@ 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 lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
public UserAuthenticationFilter(
|
public UserAuthenticationFilter(
|
||||||
|
@Lazy ApplicationProperties applicationProperties,
|
||||||
@Lazy UserService userService,
|
@Lazy UserService userService,
|
||||||
SessionPersistentRegistry sessionPersistentRegistry,
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
this.loginEnabledValue = loginEnabledValue;
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
@@ -121,33 +130,67 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// Check if the authenticated user is disabled and invalidate their session if so
|
// Check if the authenticated user is disabled and invalidate their session if so
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
|
||||||
|
Security securityProp = applicationProperties.getSecurity();
|
||||||
|
LoginMethod loginMethod = LoginMethod.UNKNOWN;
|
||||||
|
|
||||||
|
boolean blockRegistration = false;
|
||||||
|
|
||||||
|
// Extract username and determine the login method
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = null;
|
String username = null;
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails) {
|
||||||
username = ((UserDetails) principal).getUsername();
|
username = ((UserDetails) principal).getUsername();
|
||||||
|
loginMethod = LoginMethod.USERDETAILS;
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User) {
|
||||||
username = ((OAuth2User) principal).getName();
|
username = ((OAuth2User) principal).getName();
|
||||||
|
loginMethod = LoginMethod.OAUTH2USER;
|
||||||
|
OAUTH2 oAuth = securityProp.getOauth2();
|
||||||
|
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
|
loginMethod = LoginMethod.SAML2USER;
|
||||||
|
SAML2 saml2 = securityProp.getSaml2();
|
||||||
|
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String) {
|
||||||
username = (String) principal;
|
username = (String) principal;
|
||||||
|
loginMethod = LoginMethod.STRINGUSER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve all active sessions for the user
|
||||||
List<SessionInformation> sessionsInformations =
|
List<SessionInformation> sessionsInformations =
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
|
|
||||||
|
// Check if the user exists, is disabled, or needs session invalidation
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
|
log.debug("Validating user: {}", username);
|
||||||
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
boolean isUserExists = userService.usernameExistsIgnoreCase(username);
|
||||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
|
boolean notSsoLogin =
|
||||||
|
!loginMethod.equals(LoginMethod.OAUTH2USER)
|
||||||
|
&& !loginMethod.equals(LoginMethod.SAML2USER);
|
||||||
|
|
||||||
|
// Block user registration if not allowed by configuration
|
||||||
|
if (blockRegistration && !isUserExists) {
|
||||||
|
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
||||||
|
response.sendRedirect(
|
||||||
|
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire sessions and logout if the user does not exist or is disabled
|
||||||
if (!isUserExists || isUserDisabled) {
|
if (!isUserExists || isUserDisabled) {
|
||||||
|
log.info(
|
||||||
|
"Invalidating session for disabled or non-existent user: {}", username);
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||||
sessionsInformation.expireNow();
|
sessionsInformation.expireNow();
|
||||||
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUserExists) {
|
// Redirect to logout if credentials are invalid
|
||||||
|
if (!isUserExists && notSsoLogin) {
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,6 +204,25 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum LoginMethod {
|
||||||
|
USERDETAILS("UserDetails"),
|
||||||
|
OAUTH2USER("OAuth2User"),
|
||||||
|
STRINGUSER("StringUser"),
|
||||||
|
UNKNOWN("Unknown"),
|
||||||
|
SAML2USER("Saml2User");
|
||||||
|
|
||||||
|
private String method;
|
||||||
|
|
||||||
|
LoginMethod(String method) {
|
||||||
|
this.method = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -20,11 +21,12 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.*;
|
import stirling.software.SPDF.model.*;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.repository.AuthorityRepository;
|
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
private final SessionPersistentRegistry sessionRegistry;
|
private final SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
private final DatabaseBackupInterface databaseBackupHelper;
|
private final DatabaseInterface databaseService;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@@ -52,14 +54,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
PasswordEncoder passwordEncoder,
|
PasswordEncoder passwordEncoder,
|
||||||
MessageSource messageSource,
|
MessageSource messageSource,
|
||||||
SessionPersistentRegistry sessionRegistry,
|
SessionPersistentRegistry sessionRegistry,
|
||||||
DatabaseBackupInterface databaseBackupHelper,
|
DatabaseInterface databaseService,
|
||||||
ApplicationProperties applicationProperties) {
|
ApplicationProperties applicationProperties) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.authorityRepository = authorityRepository;
|
this.authorityRepository = authorityRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.messageSource = messageSource;
|
this.messageSource = messageSource;
|
||||||
this.sessionRegistry = sessionRegistry;
|
this.sessionRegistry = sessionRegistry;
|
||||||
this.databaseBackupHelper = databaseBackupHelper;
|
this.databaseService = databaseService;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
// Handle OAUTH2 login and user auto creation.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -163,12 +165,12 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType)
|
public void saveUser(String username, AuthenticationType authenticationType)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
@@ -179,11 +181,11 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.addAuthority(new Authority(role, user));
|
user.addAuthority(new Authority(role, user));
|
||||||
user.setAuthenticationType(authenticationType);
|
user.setAuthenticationType(authenticationType);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password)
|
public void saveUser(String username, String password)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
@@ -193,11 +195,11 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setEnabled(true);
|
user.setEnabled(true);
|
||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role, boolean firstLogin)
|
public void saveUser(String username, String password, String role, boolean firstLogin)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
@@ -209,11 +211,11 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
user.setFirstLogin(firstLogin);
|
user.setFirstLogin(firstLogin);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void saveUser(String username, String password, String role)
|
public void saveUser(String username, String password, String role)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
saveUser(username, password, role, false);
|
saveUser(username, password, role, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +249,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 SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -259,7 +261,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
settingsMap.putAll(updates);
|
settingsMap.putAll(updates);
|
||||||
user.setSettings(settingsMap);
|
user.setSettings(settingsMap);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,38 +282,45 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void changeUsername(User user, String newUsername)
|
public void changeUsername(User user, String newUsername)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException,
|
||||||
|
IOException,
|
||||||
|
SQLException,
|
||||||
|
UnsupportedProviderException {
|
||||||
if (!isUsernameValid(newUsername)) {
|
if (!isUsernameValid(newUsername)) {
|
||||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||||
}
|
}
|
||||||
user.setUsername(newUsername);
|
user.setUsername(newUsername);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changePassword(User user, String newPassword) throws IOException {
|
public void changePassword(User user, String newPassword)
|
||||||
|
throws SQLException, UnsupportedProviderException {
|
||||||
user.setPassword(passwordEncoder.encode(newPassword));
|
user.setPassword(passwordEncoder.encode(newPassword));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeFirstUse(User user, boolean firstUse) throws IOException {
|
public void changeFirstUse(User user, boolean firstUse)
|
||||||
|
throws SQLException, UnsupportedProviderException {
|
||||||
user.setFirstLogin(firstUse);
|
user.setFirstLogin(firstUse);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeRole(User user, String newRole) throws IOException {
|
public void changeRole(User user, String newRole)
|
||||||
|
throws SQLException, UnsupportedProviderException {
|
||||||
Authority userAuthority = this.findRole(user);
|
Authority userAuthority = this.findRole(user);
|
||||||
userAuthority.setAuthority(newRole);
|
userAuthority.setAuthority(newRole);
|
||||||
authorityRepository.save(userAuthority);
|
authorityRepository.save(userAuthority);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
|
public void changeUserEnabled(User user, Boolean enbeled)
|
||||||
|
throws SQLException, UnsupportedProviderException {
|
||||||
user.setEnabled(enbeled);
|
user.setEnabled(enbeled);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||||
@@ -397,7 +406,8 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void syncCustomApiUser(String customApiKey) throws IOException {
|
public void syncCustomApiUser(String customApiKey)
|
||||||
|
throws SQLException, UnsupportedProviderException {
|
||||||
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -414,14 +424,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
user.setApiKey(customApiKey);
|
user.setApiKey(customApiKey);
|
||||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
} else {
|
} else {
|
||||||
// Update API key if it has changed
|
// Update API key if it has changed
|
||||||
User user = existingUser.get();
|
User user = existingUser.get();
|
||||||
if (!customApiKey.equals(user.getApiKey())) {
|
if (!customApiKey.equals(user.getApiKey())) {
|
||||||
user.setApiKey(customApiKey);
|
user.setApiKey(customApiKey);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,232 +0,0 @@
|
|||||||
package stirling.software.SPDF.config.security.database;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.DirectoryStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.sql.*;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneId;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Configuration
|
|
||||||
public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|
||||||
|
|
||||||
@Value("${spring.datasource.url}")
|
|
||||||
private String url;
|
|
||||||
|
|
||||||
@Value("${spring.datasource.username}")
|
|
||||||
private String databaseUsername;
|
|
||||||
|
|
||||||
@Value("${spring.datasource.password}")
|
|
||||||
private String databasePassword;
|
|
||||||
|
|
||||||
private Path backupPath = Paths.get("configs/db/backup/");
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasBackup() {
|
|
||||||
// Check if there is at least one backup
|
|
||||||
return !getBackupList().isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<FileInfo> getBackupList() {
|
|
||||||
// Check if the backup directory exists, and create it if it does not
|
|
||||||
ensureBackupDirectoryExists();
|
|
||||||
|
|
||||||
List<FileInfo> backupFiles = new ArrayList<>();
|
|
||||||
|
|
||||||
// Read the backup directory and filter for files with the prefix "backup_" and suffix
|
|
||||||
// ".sql"
|
|
||||||
try (DirectoryStream<Path> stream =
|
|
||||||
Files.newDirectoryStream(
|
|
||||||
backupPath,
|
|
||||||
path ->
|
|
||||||
path.getFileName().toString().startsWith("backup_")
|
|
||||||
&& path.getFileName().toString().endsWith(".sql"))) {
|
|
||||||
for (Path entry : stream) {
|
|
||||||
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
|
|
||||||
LocalDateTime modificationDate =
|
|
||||||
LocalDateTime.ofInstant(
|
|
||||||
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
|
|
||||||
LocalDateTime creationDate =
|
|
||||||
LocalDateTime.ofInstant(
|
|
||||||
attrs.creationTime().toInstant(), ZoneId.systemDefault());
|
|
||||||
long fileSize = attrs.size();
|
|
||||||
backupFiles.add(
|
|
||||||
new FileInfo(
|
|
||||||
entry.getFileName().toString(),
|
|
||||||
entry.toString(),
|
|
||||||
modificationDate,
|
|
||||||
fileSize,
|
|
||||||
creationDate));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error reading backup directory: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return backupFiles;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imports a database backup from the specified file.
|
|
||||||
public boolean importDatabaseFromUI(String fileName) throws IOException {
|
|
||||||
return this.importDatabaseFromUI(getBackupFilePath(fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imports a database backup from the specified path.
|
|
||||||
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
|
|
||||||
boolean success = executeDatabaseScript(tempTemplatePath);
|
|
||||||
if (success) {
|
|
||||||
LocalDateTime dateNow = LocalDateTime.now();
|
|
||||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
|
||||||
Path insertOutputFilePath =
|
|
||||||
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
|
|
||||||
Files.copy(tempTemplatePath, insertOutputFilePath);
|
|
||||||
Files.deleteIfExists(tempTemplatePath);
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean importDatabase() {
|
|
||||||
if (!this.hasBackup()) return false;
|
|
||||||
|
|
||||||
List<FileInfo> backupList = this.getBackupList();
|
|
||||||
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
|
||||||
|
|
||||||
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void exportDatabase() throws IOException {
|
|
||||||
// Check if the backup directory exists, and create it if it does not
|
|
||||||
ensureBackupDirectoryExists();
|
|
||||||
|
|
||||||
// Filter and delete old backups if there are more than 5
|
|
||||||
List<FileInfo> filteredBackupList =
|
|
||||||
this.getBackupList().stream()
|
|
||||||
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (filteredBackupList.size() > 5) {
|
|
||||||
filteredBackupList.sort(
|
|
||||||
Comparator.comparing(
|
|
||||||
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
|
||||||
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
|
|
||||||
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
|
|
||||||
}
|
|
||||||
|
|
||||||
LocalDateTime dateNow = LocalDateTime.now();
|
|
||||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
|
||||||
Path insertOutputFilePath =
|
|
||||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
|
||||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
|
||||||
|
|
||||||
try (Connection conn =
|
|
||||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
|
||||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
|
||||||
stmt.setString(1, insertOutputFilePath.toString());
|
|
||||||
stmt.execute();
|
|
||||||
log.info("Database export completed: {}", insertOutputFilePath);
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Error during database export: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieves the H2 database version.
|
|
||||||
public String getH2Version() {
|
|
||||||
String version = "Unknown";
|
|
||||||
try (Connection conn =
|
|
||||||
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
|
||||||
try (Statement stmt = conn.createStatement();
|
|
||||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
|
||||||
if (rs.next()) {
|
|
||||||
version = rs.getString("version");
|
|
||||||
log.info("H2 Database Version: {}", version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes a backup file.
|
|
||||||
public boolean deleteBackupFile(String fileName) throws IOException {
|
|
||||||
if (!isValidFileName(fileName)) {
|
|
||||||
log.error("Invalid file name: {}", fileName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Path filePath = this.getBackupFilePath(fileName);
|
|
||||||
if (Files.deleteIfExists(filePath)) {
|
|
||||||
log.info("Deleted backup file: {}", fileName);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
log.error("File not found or could not be deleted: {}", fileName);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the Path object for a given backup file name.
|
|
||||||
public Path getBackupFilePath(String fileName) {
|
|
||||||
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
|
|
||||||
if (!filePath.startsWith(backupPath)) {
|
|
||||||
throw new SecurityException("Path traversal detected");
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean executeDatabaseScript(Path scriptPath) {
|
|
||||||
String query = "RUNSCRIPT from ?;";
|
|
||||||
|
|
||||||
try (Connection conn =
|
|
||||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
|
||||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
|
||||||
stmt.setString(1, scriptPath.toString());
|
|
||||||
stmt.execute();
|
|
||||||
log.info("Database import completed: {}", scriptPath);
|
|
||||||
return true;
|
|
||||||
} catch (SQLException e) {
|
|
||||||
log.error("Error during database import: {}", e.getMessage(), e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ensureBackupDirectoryExists() {
|
|
||||||
if (Files.notExists(backupPath)) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(backupPath);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error creating directories: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("|");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Getter
|
||||||
|
@Configuration
|
||||||
|
public class DatabaseConfig {
|
||||||
|
|
||||||
|
public static final String DATASOURCE_DEFAULT_URL =
|
||||||
|
"jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
||||||
|
public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s";
|
||||||
|
public static final String DEFAULT_DRIVER = "org.h2.Driver";
|
||||||
|
public static final String DEFAULT_USERNAME = "sa";
|
||||||
|
public static final String POSTGRES_DRIVER = "org.postgresql.Driver";
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final boolean runningEE;
|
||||||
|
|
||||||
|
public DatabaseConfig(
|
||||||
|
ApplicationProperties applicationProperties,
|
||||||
|
@Qualifier("runningEE") boolean runningEE) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.runningEE = runningEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the <code>DataSource</code> for the connection to the DB. If <code>useDefault</code>
|
||||||
|
* is set to <code>true</code>, it will use the default H2 DB. If it is set to <code>false
|
||||||
|
* </code>, it will use the user's custom configuration set in the settings.yml.
|
||||||
|
*
|
||||||
|
* @return a <code>DataSource</code> using the configuration settings in the settings.yml
|
||||||
|
* @throws UnsupportedProviderException if the type of database selected is not supported
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Qualifier("dataSource")
|
||||||
|
public DataSource dataSource() throws UnsupportedProviderException {
|
||||||
|
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
|
||||||
|
|
||||||
|
if (!runningEE) {
|
||||||
|
return useDefaultDataSource(dataSourceBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationProperties.System system = applicationProperties.getSystem();
|
||||||
|
ApplicationProperties.Datasource datasource = system.getDatasource();
|
||||||
|
|
||||||
|
if (!datasource.isEnableCustomDatabase()) {
|
||||||
|
return useDefaultDataSource(dataSourceBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Using custom database configuration");
|
||||||
|
|
||||||
|
if (!datasource.getCustomDatabaseUrl().isBlank()) {
|
||||||
|
if (datasource.getCustomDatabaseUrl().contains("postgresql")) {
|
||||||
|
dataSourceBuilder.driverClassName(POSTGRES_DRIVER);
|
||||||
|
}
|
||||||
|
|
||||||
|
dataSourceBuilder.url(datasource.getCustomDatabaseUrl());
|
||||||
|
} else {
|
||||||
|
dataSourceBuilder.driverClassName(getDriverClassName(datasource.getType()));
|
||||||
|
dataSourceBuilder.url(
|
||||||
|
generateCustomDataSourceUrl(
|
||||||
|
datasource.getType(),
|
||||||
|
datasource.getHostName(),
|
||||||
|
datasource.getPort(),
|
||||||
|
datasource.getName()));
|
||||||
|
}
|
||||||
|
dataSourceBuilder.username(datasource.getUsername());
|
||||||
|
dataSourceBuilder.password(datasource.getPassword());
|
||||||
|
|
||||||
|
return dataSourceBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
|
||||||
|
log.info("Using default H2 database");
|
||||||
|
|
||||||
|
dataSourceBuilder.url(DATASOURCE_DEFAULT_URL);
|
||||||
|
dataSourceBuilder.username(DEFAULT_USERNAME);
|
||||||
|
|
||||||
|
return dataSourceBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the URL the <code>DataSource</code> will use to connect to the database
|
||||||
|
*
|
||||||
|
* @param dataSourceType the type of the database
|
||||||
|
* @param hostname the host name
|
||||||
|
* @param port the port number to use for the database
|
||||||
|
* @param dataSourceName the name the database to connect to
|
||||||
|
* @return the <code>DataSource</code> URL
|
||||||
|
*/
|
||||||
|
private String generateCustomDataSourceUrl(
|
||||||
|
String dataSourceType, String hostname, Integer port, String dataSourceName) {
|
||||||
|
return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects the database driver based on the type of database chosen.
|
||||||
|
*
|
||||||
|
* @param driverName the type of the driver (e.g. 'h2', 'postgresql')
|
||||||
|
* @return the fully qualified driver for the database chosen
|
||||||
|
* @throws UnsupportedProviderException when an unsupported database is selected
|
||||||
|
*/
|
||||||
|
private String getDriverClassName(String driverName) throws UnsupportedProviderException {
|
||||||
|
try {
|
||||||
|
ApplicationProperties.Driver driver =
|
||||||
|
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
||||||
|
|
||||||
|
switch (driver) {
|
||||||
|
case H2 -> {
|
||||||
|
log.debug("H2 driver selected");
|
||||||
|
return DEFAULT_DRIVER;
|
||||||
|
}
|
||||||
|
case POSTGRESQL -> {
|
||||||
|
log.debug("Postgres driver selected");
|
||||||
|
return POSTGRES_DRIVER;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
log.warn("{} driver selected", driverName);
|
||||||
|
throw new UnsupportedProviderException(
|
||||||
|
driverName + " is not currently supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Unknown driver: {}", driverName);
|
||||||
|
throw new UnsupportedProviderException(driverName + " is not currently supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
|
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
||||||
|
import org.springframework.jdbc.datasource.init.ScriptException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.exception.BackupNotFoundException;
|
||||||
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class DatabaseService implements DatabaseInterface {
|
||||||
|
|
||||||
|
public static final String BACKUP_PREFIX = "backup_";
|
||||||
|
public static final String SQL_SUFFIX = ".sql";
|
||||||
|
private static final String BACKUP_DIR = "configs/db/backup/";
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final DataSource dataSource;
|
||||||
|
|
||||||
|
public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) {
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.dataSource = dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there is at least one backup. First checks if the directory exists, then checks if
|
||||||
|
* there are backup scripts within the directory
|
||||||
|
*
|
||||||
|
* @return true if there are backup scripts, false if there are not
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean hasBackup() {
|
||||||
|
Path filePath = Paths.get(BACKUP_DIR);
|
||||||
|
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
return !getBackupList().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the backup directory and filter for files with the prefix "backup_" and suffix ".sql"
|
||||||
|
*
|
||||||
|
* @return a <code>List</code> of backup files
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<FileInfo> getBackupList() {
|
||||||
|
List<FileInfo> backupFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
if (isH2Database()) {
|
||||||
|
Path backupPath = Paths.get(BACKUP_DIR);
|
||||||
|
|
||||||
|
try (DirectoryStream<Path> stream =
|
||||||
|
Files.newDirectoryStream(
|
||||||
|
backupPath,
|
||||||
|
path ->
|
||||||
|
path.getFileName().toString().startsWith(BACKUP_PREFIX)
|
||||||
|
&& path.getFileName()
|
||||||
|
.toString()
|
||||||
|
.endsWith(SQL_SUFFIX))) {
|
||||||
|
for (Path entry : stream) {
|
||||||
|
BasicFileAttributes attrs =
|
||||||
|
Files.readAttributes(entry, BasicFileAttributes.class);
|
||||||
|
LocalDateTime modificationDate =
|
||||||
|
LocalDateTime.ofInstant(
|
||||||
|
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
|
||||||
|
LocalDateTime creationDate =
|
||||||
|
LocalDateTime.ofInstant(
|
||||||
|
attrs.creationTime().toInstant(), ZoneId.systemDefault());
|
||||||
|
long fileSize = attrs.size();
|
||||||
|
backupFiles.add(
|
||||||
|
new FileInfo(
|
||||||
|
entry.getFileName().toString(),
|
||||||
|
entry.toString(),
|
||||||
|
modificationDate,
|
||||||
|
fileSize,
|
||||||
|
creationDate));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error reading backup directory: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return backupFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void importDatabase() {
|
||||||
|
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
|
||||||
|
|
||||||
|
List<FileInfo> backupList = this.getBackupList();
|
||||||
|
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
||||||
|
|
||||||
|
Path latestExport = Paths.get(backupList.get(0).getFilePath());
|
||||||
|
|
||||||
|
executeDatabaseScript(latestExport);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Imports a database backup from the specified file. */
|
||||||
|
public boolean importDatabaseFromUI(String fileName) {
|
||||||
|
try {
|
||||||
|
importDatabaseFromUI(getBackupFilePath(fileName));
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error(
|
||||||
|
"Error importing database from file: {}, message: {}",
|
||||||
|
fileName,
|
||||||
|
e.getMessage(),
|
||||||
|
e.getCause());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Imports a database backup from the specified path. */
|
||||||
|
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
|
||||||
|
executeDatabaseScript(tempTemplatePath);
|
||||||
|
LocalDateTime dateNow = LocalDateTime.now();
|
||||||
|
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||||
|
Path insertOutputFilePath =
|
||||||
|
this.getBackupFilePath(
|
||||||
|
BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
|
||||||
|
Files.copy(tempTemplatePath, insertOutputFilePath);
|
||||||
|
Files.deleteIfExists(tempTemplatePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportDatabase() {
|
||||||
|
List<FileInfo> filteredBackupList =
|
||||||
|
this.getBackupList().stream()
|
||||||
|
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (filteredBackupList.size() > 5) {
|
||||||
|
deleteOldestBackup(filteredBackupList);
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime dateNow = LocalDateTime.now();
|
||||||
|
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||||
|
Path insertOutputFilePath =
|
||||||
|
this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX);
|
||||||
|
|
||||||
|
if (isH2Database()) {
|
||||||
|
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||||
|
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
stmt.setString(1, insertOutputFilePath.toString());
|
||||||
|
stmt.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error during database export: {}", e.getMessage(), e);
|
||||||
|
} catch (CannotReadScriptException e) {
|
||||||
|
log.error("Error during database export: File {} not found", insertOutputFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Database export completed: {}", insertOutputFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
|
||||||
|
try {
|
||||||
|
filteredBackupList.sort(
|
||||||
|
Comparator.comparing(
|
||||||
|
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
||||||
|
|
||||||
|
FileInfo oldestFile = filteredBackupList.get(0);
|
||||||
|
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
|
||||||
|
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the H2 database version.
|
||||||
|
*
|
||||||
|
* @return <code>String</code> of the H2 version
|
||||||
|
*/
|
||||||
|
public String getH2Version() {
|
||||||
|
String version = "Unknown";
|
||||||
|
|
||||||
|
if (isH2Database()) {
|
||||||
|
try (Connection conn = dataSource.getConnection()) {
|
||||||
|
try (Statement stmt = conn.createStatement();
|
||||||
|
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||||
|
if (rs.next()) {
|
||||||
|
version = rs.getString("version");
|
||||||
|
log.info("H2 Database Version: {}", version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isH2Database() {
|
||||||
|
ApplicationProperties.Datasource datasource =
|
||||||
|
applicationProperties.getSystem().getDatasource();
|
||||||
|
return !datasource.isEnableCustomDatabase()
|
||||||
|
|| datasource.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a backup file.
|
||||||
|
*
|
||||||
|
* @return true if successful, false if not
|
||||||
|
*/
|
||||||
|
public boolean deleteBackupFile(String fileName) throws IOException {
|
||||||
|
if (!isValidFileName(fileName)) {
|
||||||
|
log.error("Invalid file name: {}", fileName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Path filePath = this.getBackupFilePath(fileName);
|
||||||
|
if (Files.deleteIfExists(filePath)) {
|
||||||
|
log.info("Deleted backup file: {}", fileName);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.error("File not found or could not be deleted: {}", fileName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Path for a given backup file name.
|
||||||
|
*
|
||||||
|
* @return the <code>Path</code> object for the given file name
|
||||||
|
*/
|
||||||
|
public Path getBackupFilePath(String fileName) {
|
||||||
|
Path filePath = Paths.get(BACKUP_DIR, fileName).normalize();
|
||||||
|
if (!filePath.startsWith(BACKUP_DIR)) {
|
||||||
|
throw new SecurityException("Path traversal detected");
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void executeDatabaseScript(Path scriptPath) {
|
||||||
|
if (isH2Database()) {
|
||||||
|
String query = "RUNSCRIPT from ?;";
|
||||||
|
|
||||||
|
try (Connection conn = dataSource.getConnection();
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
|
stmt.setString(1, scriptPath.toString());
|
||||||
|
stmt.execute();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
log.error("Error during database import: {}", e.getMessage(), e);
|
||||||
|
} catch (ScriptException e) {
|
||||||
|
log.error("Error: File {} not found", scriptPath.toString(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Database import completed: {}", scriptPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks for invalid characters or sequences
|
||||||
|
*
|
||||||
|
* @return true if it contains no invalid characters, false if it does
|
||||||
|
*/
|
||||||
|
private boolean isValidFileName(String fileName) {
|
||||||
|
return fileName != null
|
||||||
|
&& !fileName.contains("..")
|
||||||
|
&& !fileName.contains("/")
|
||||||
|
&& !fileName.contains("\\")
|
||||||
|
&& !fileName.contains(":")
|
||||||
|
&& !fileName.contains("*")
|
||||||
|
&& !fileName.contains("?")
|
||||||
|
&& !fileName.contains("\"")
|
||||||
|
&& !fileName.contains("<")
|
||||||
|
&& !fileName.contains(">")
|
||||||
|
&& !fileName.contains("|");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,27 @@
|
|||||||
package stirling.software.SPDF.config.security.database;
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
|
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@Conditional(H2SQLCondition.class)
|
||||||
public class ScheduledTasks {
|
public class ScheduledTasks {
|
||||||
|
|
||||||
private final DatabaseBackupHelper databaseBackupService;
|
private final DatabaseInterface databaseService;
|
||||||
|
|
||||||
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
|
public ScheduledTasks(DatabaseInterface databaseService) {
|
||||||
this.databaseBackupService = databaseBackupService;
|
this.databaseService = databaseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 0 * * ?")
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
public void performBackup() throws IOException {
|
public void performBackup() throws SQLException, UnsupportedProviderException {
|
||||||
databaseBackupService.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.config.security.oauth2;
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService;
|
|||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
public class CustomOAuth2AuthenticationSuccessHandler
|
public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
@@ -97,10 +99,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||||
}
|
}
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
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.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
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.oauth2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public class OAuth2Configuration {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
@Lazy private final UserService userService;
|
||||||
|
|
||||||
|
public OAuth2Configuration(
|
||||||
|
ApplicationProperties applicationProperties, @Lazy UserService userService) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
package stirling.software.SPDF.config.security.saml2;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
import org.springframework.security.authentication.LockedException;
|
import org.springframework.security.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
@@ -18,6 +19,7 @@ import stirling.software.SPDF.config.security.UserService;
|
|||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -109,7 +111,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
log.debug("Successfully processed authentication for user: {}", username);
|
log.debug("Successfully processed authentication for user: {}", username);
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
return;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Invalid username detected for user: {}, redirecting to logout",
|
"Invalid username detected for user: {}, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import org.springframework.core.convert.converter.Converter;
|
|||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Component
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomSaml2ResponseAuthenticationConverter
|
public class CustomSaml2ResponseAuthenticationConverter
|
||||||
implements Converter<ResponseToken, Saml2Authentication> {
|
implements Converter<ResponseToken, Saml2Authentication> {
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml2;
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||||
|
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.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||||
|
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.registration.Saml2MessageBinding;
|
||||||
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public class SAML2Configuration {
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
||||||
|
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
name = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||||
|
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||||
|
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||||
|
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||||
|
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||||
|
Resource certificateResource = samlConf.getSpCert();
|
||||||
|
Saml2X509Credential signingCredential =
|
||||||
|
new Saml2X509Credential(
|
||||||
|
CertificateUtils.readPrivateKey(privateKeyResource),
|
||||||
|
CertificateUtils.readCertificate(certificateResource),
|
||||||
|
Saml2X509CredentialType.SIGNING);
|
||||||
|
RelyingPartyRegistration rp =
|
||||||
|
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||||
|
.signingX509Credentials(c -> c.add(signingCredential))
|
||||||
|
.assertingPartyMetadata(
|
||||||
|
metadata ->
|
||||||
|
metadata.entityId(samlConf.getIdpIssuer())
|
||||||
|
.singleSignOnServiceLocation(
|
||||||
|
samlConf.getIdpSingleLoginUrl())
|
||||||
|
.verificationX509Credentials(
|
||||||
|
c -> c.add(verificationCredential))
|
||||||
|
.singleSignOnServiceBinding(
|
||||||
|
Saml2MessageBinding.POST)
|
||||||
|
.wantAuthnRequestsSigned(true))
|
||||||
|
.build();
|
||||||
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
name = "security.saml2.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||||
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||||
|
OpenSaml4AuthenticationRequestResolver resolver =
|
||||||
|
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||||
|
resolver.setAuthnRequestCustomizer(
|
||||||
|
customizer -> {
|
||||||
|
log.debug("Customizing SAML Authentication request");
|
||||||
|
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||||
|
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||||
|
if (authnRequest.getID() == null) {
|
||||||
|
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||||
|
}
|
||||||
|
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||||
|
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||||
|
log.debug(
|
||||||
|
"AuthnRequest Issuer: {}",
|
||||||
|
authnRequest.getIssuer() != null
|
||||||
|
? authnRequest.getIssuer().getValue()
|
||||||
|
: "null");
|
||||||
|
HttpServletRequest request = customizer.getRequest();
|
||||||
|
// Log HTTP request details
|
||||||
|
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||||
|
log.debug("Request URI: {}", request.getRequestURI());
|
||||||
|
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||||
|
log.debug("Query String: {}", request.getQueryString());
|
||||||
|
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||||
|
// Log headers
|
||||||
|
Collections.list(request.getHeaderNames())
|
||||||
|
.forEach(
|
||||||
|
headerName -> {
|
||||||
|
log.debug(
|
||||||
|
"Header - {}: {}",
|
||||||
|
headerName,
|
||||||
|
request.getHeader(headerName));
|
||||||
|
});
|
||||||
|
// Log SAML specific parameters
|
||||||
|
log.debug("SAML Request Parameters:");
|
||||||
|
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||||
|
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||||
|
// Log session debugrmation if exists
|
||||||
|
if (request.getSession(false) != null) {
|
||||||
|
log.debug("Session ID: {}", request.getSession().getId());
|
||||||
|
}
|
||||||
|
// Log any assertions consumer service details if present
|
||||||
|
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||||
|
log.debug(
|
||||||
|
"AssertionConsumerServiceURL: {}",
|
||||||
|
authnRequest.getAssertionConsumerServiceURL());
|
||||||
|
}
|
||||||
|
// Log NameID policy if present
|
||||||
|
if (authnRequest.getNameIDPolicy() != null) {
|
||||||
|
log.debug(
|
||||||
|
"NameIDPolicy Format: {}",
|
||||||
|
authnRequest.getNameIDPolicy().getFormat());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return resolver;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
import org.eclipse.jetty.http.HttpStatus;
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -24,19 +25,20 @@ import io.swagger.v3.oas.annotations.Parameter;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
import stirling.software.SPDF.config.security.database.DatabaseService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Controller
|
@Controller
|
||||||
@RequestMapping("/api/v1/database")
|
@RequestMapping("/api/v1/database")
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
|
@Conditional(H2SQLCondition.class)
|
||||||
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||||
public class DatabaseController {
|
public class DatabaseController {
|
||||||
|
|
||||||
private final DatabaseBackupHelper databaseBackupHelper;
|
private final DatabaseService databaseService;
|
||||||
|
|
||||||
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
|
public DatabaseController(DatabaseService databaseService) {
|
||||||
this.databaseBackupHelper = databaseBackupHelper;
|
this.databaseService = databaseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -57,7 +59,7 @@ public class DatabaseController {
|
|||||||
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
|
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
|
||||||
try (InputStream in = file.getInputStream()) {
|
try (InputStream in = file.getInputStream()) {
|
||||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
|
boolean importSuccess = databaseService.importDatabaseFromUI(tempTemplatePath);
|
||||||
if (importSuccess) {
|
if (importSuccess) {
|
||||||
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
|
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
|
||||||
} else {
|
} else {
|
||||||
@@ -77,21 +79,20 @@ public class DatabaseController {
|
|||||||
@GetMapping("/import-database-file/{fileName}")
|
@GetMapping("/import-database-file/{fileName}")
|
||||||
public String importDatabaseFromBackupUI(
|
public String importDatabaseFromBackupUI(
|
||||||
@Parameter(description = "Name of the file to import", required = true) @PathVariable
|
@Parameter(description = "Name of the file to import", required = true) @PathVariable
|
||||||
String fileName)
|
String fileName) {
|
||||||
throws IOException {
|
|
||||||
if (fileName == null || fileName.isEmpty()) {
|
if (fileName == null || fileName.isEmpty()) {
|
||||||
return "redirect:/database?error=fileNullOrEmpty";
|
return "redirect:/database?error=fileNullOrEmpty";
|
||||||
}
|
}
|
||||||
// Check if the file exists in the backup list
|
// Check if the file exists in the backup list
|
||||||
boolean fileExists =
|
boolean fileExists =
|
||||||
databaseBackupHelper.getBackupList().stream()
|
databaseService.getBackupList().stream()
|
||||||
.anyMatch(backup -> backup.getFileName().equals(fileName));
|
.anyMatch(backup -> backup.getFileName().equals(fileName));
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
log.error("File {} not found in backup list", fileName);
|
log.error("File {} not found in backup list", fileName);
|
||||||
return "redirect:/database?error=fileNotFound";
|
return "redirect:/database?error=fileNotFound";
|
||||||
}
|
}
|
||||||
log.info("Received file: {}", fileName);
|
log.info("Received file: {}", fileName);
|
||||||
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
|
if (databaseService.importDatabaseFromUI(fileName)) {
|
||||||
log.info("File {} imported to database", fileName);
|
log.info("File {} imported to database", fileName);
|
||||||
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
|
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ public class DatabaseController {
|
|||||||
throw new IllegalArgumentException("File must not be null or empty");
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (databaseBackupHelper.deleteBackupFile(fileName)) {
|
if (databaseService.deleteBackupFile(fileName)) {
|
||||||
log.info("Deleted file: {}", fileName);
|
log.info("Deleted file: {}", fileName);
|
||||||
} else {
|
} else {
|
||||||
log.error("Failed to delete file: {}", fileName);
|
log.error("Failed to delete file: {}", fileName);
|
||||||
@@ -135,7 +136,7 @@ public class DatabaseController {
|
|||||||
throw new IllegalArgumentException("File must not be null or empty");
|
throw new IllegalArgumentException("File must not be null or empty");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
|
Path filePath = databaseService.getBackupFilePath(fileName);
|
||||||
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
|
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
|
||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
|
||||||
@@ -157,14 +158,9 @@ public class DatabaseController {
|
|||||||
+ " database management page.")
|
+ " database management page.")
|
||||||
@GetMapping("/createDatabaseBackup")
|
@GetMapping("/createDatabaseBackup")
|
||||||
public String createDatabaseBackup() {
|
public String createDatabaseBackup() {
|
||||||
try {
|
|
||||||
log.info("Starting database backup creation...");
|
log.info("Starting database backup creation...");
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
log.info("Database backup successfully created.");
|
log.info("Database backup successfully created.");
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error creating database backup: {}", e.getMessage(), e);
|
|
||||||
return "redirect:/database?error=" + e.getMessage();
|
|
||||||
}
|
|
||||||
return "redirect:/database?infoMessage=backupCreated";
|
return "redirect:/database?infoMessage=backupCreated";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Condition;
|
||||||
|
import org.springframework.context.annotation.ConditionContext;
|
||||||
|
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||||
|
|
||||||
|
public class H2SQLCondition implements Condition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||||
|
boolean enableCustomDatabase =
|
||||||
|
Boolean.parseBoolean(
|
||||||
|
context.getEnvironment()
|
||||||
|
.getProperty("system.datasource.enableCustomDatabase"));
|
||||||
|
String dataSourceType = context.getEnvironment().getProperty("system.datasource.type");
|
||||||
|
return !enableCustomDatabase
|
||||||
|
|| (enableCustomDatabase && "h2".equalsIgnoreCase(dataSourceType));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
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.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@@ -33,7 +34,8 @@ public class SettingsController {
|
|||||||
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||||
.body(
|
.body(
|
||||||
"Setting has already been set, To adjust please edit /config/settings.yml");
|
"Setting has already been set, To adjust please edit "
|
||||||
|
+ InstallationPathConfig.getSettingsPath());
|
||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.sql.SQLException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -33,6 +34,7 @@ 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;
|
||||||
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "User", description = "User APIs")
|
@Tag(name = "User", description = "User APIs")
|
||||||
@@ -52,7 +54,7 @@ public class UserController {
|
|||||||
@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)
|
||||||
throws IOException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
||||||
model.addAttribute("error", "Username already exists");
|
model.addAttribute("error", "Username already exists");
|
||||||
return "register";
|
return "register";
|
||||||
@@ -74,7 +76,7 @@ public class UserController {
|
|||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes)
|
RedirectAttributes redirectAttributes)
|
||||||
throws IOException {
|
throws IOException, SQLException, UnsupportedProviderException {
|
||||||
if (!userService.isUsernameValid(newUsername)) {
|
if (!userService.isUsernameValid(newUsername)) {
|
||||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,7 @@ public class UserController {
|
|||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes)
|
RedirectAttributes redirectAttributes)
|
||||||
throws IOException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
@@ -145,7 +147,7 @@ public class UserController {
|
|||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
RedirectAttributes redirectAttributes)
|
RedirectAttributes redirectAttributes)
|
||||||
throws IOException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
@@ -166,7 +168,7 @@ public class UserController {
|
|||||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||||
@PostMapping("/updateUserSettings")
|
@PostMapping("/updateUserSettings")
|
||||||
public String updateUserSettings(HttpServletRequest request, Principal principal)
|
public String updateUserSettings(HttpServletRequest request, Principal principal)
|
||||||
throws IOException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
Map<String, String> updates = new HashMap<>();
|
Map<String, String> updates = new HashMap<>();
|
||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
@@ -188,7 +190,7 @@ public class UserController {
|
|||||||
@RequestParam(name = "authType") String authType,
|
@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, SQLException, UnsupportedProviderException {
|
||||||
if (!userService.isUsernameValid(username)) {
|
if (!userService.isUsernameValid(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||||
}
|
}
|
||||||
@@ -232,7 +234,7 @@ public class UserController {
|
|||||||
@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 {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (!userOpt.isPresent()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
@@ -270,7 +272,7 @@ public class UserController {
|
|||||||
@PathVariable("username") String username,
|
@PathVariable("username") String username,
|
||||||
@RequestParam("enabled") boolean enabled,
|
@RequestParam("enabled") boolean enabled,
|
||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws IOException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (!userOpt.isPresent()) {
|
if (!userOpt.isPresent()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.pdfbox.Loader;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.rendering.ImageType;
|
import org.apache.pdfbox.rendering.ImageType;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -31,11 +34,8 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
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.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
import stirling.software.SPDF.utils.*;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
|
||||||
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;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@@ -62,14 +62,20 @@ public class ConvertImgPDFController {
|
|||||||
String singleOrMultiple = request.getSingleOrMultiple();
|
String singleOrMultiple = request.getSingleOrMultiple();
|
||||||
String colorType = request.getColorType();
|
String colorType = request.getColorType();
|
||||||
String dpi = request.getDpi();
|
String dpi = request.getDpi();
|
||||||
|
String pageNumbers = request.getPageNumbers();
|
||||||
Path tempFile = null;
|
Path tempFile = null;
|
||||||
Path tempOutputDir = null;
|
Path tempOutputDir = null;
|
||||||
Path tempPdfPath = null;
|
Path tempPdfPath = null;
|
||||||
byte[] result = null;
|
byte[] result = null;
|
||||||
|
String[] pageOrderArr =
|
||||||
|
(pageNumbers != null && !pageNumbers.trim().isEmpty())
|
||||||
|
? pageNumbers.split(",")
|
||||||
|
: new String[] {"all"};
|
||||||
|
;
|
||||||
try {
|
try {
|
||||||
byte[] pdfBytes = file.getBytes();
|
// Load the input PDF
|
||||||
|
byte[] newPdfBytes = rearrangePdfPages(file.getBytes(), pageOrderArr);
|
||||||
|
|
||||||
ImageType colorTypeResult = ImageType.RGB;
|
ImageType colorTypeResult = ImageType.RGB;
|
||||||
if ("greyscale".equals(colorType)) {
|
if ("greyscale".equals(colorType)) {
|
||||||
colorTypeResult = ImageType.GRAY;
|
colorTypeResult = ImageType.GRAY;
|
||||||
@@ -84,7 +90,7 @@ public class ConvertImgPDFController {
|
|||||||
|
|
||||||
result =
|
result =
|
||||||
PdfUtils.convertFromPdf(
|
PdfUtils.convertFromPdf(
|
||||||
pdfBytes,
|
newPdfBytes,
|
||||||
"webp".equalsIgnoreCase(imageFormat)
|
"webp".equalsIgnoreCase(imageFormat)
|
||||||
? "png"
|
? "png"
|
||||||
: imageFormat.toUpperCase(),
|
: imageFormat.toUpperCase(),
|
||||||
@@ -227,4 +233,46 @@ public class ConvertImgPDFController {
|
|||||||
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
||||||
return "null".equals(mimeType) ? "application/octet-stream" : mimeType;
|
return "null".equals(mimeType) ? "application/octet-stream" : mimeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rearranges the pages of the given PDF document based on the specified page order.
|
||||||
|
*
|
||||||
|
* @param pdfBytes The byte array of the original PDF file.
|
||||||
|
* @param pageOrderArr An array of page numbers indicating the new order.
|
||||||
|
* @return A byte array of the rearranged PDF.
|
||||||
|
* @throws IOException If an error occurs while processing the PDF.
|
||||||
|
*/
|
||||||
|
private byte[] rearrangePdfPages(byte[] pdfBytes, String[] pageOrderArr) throws IOException {
|
||||||
|
// Load the input PDF
|
||||||
|
PDDocument document = Loader.loadPDF(pdfBytes);
|
||||||
|
int totalPages = document.getNumberOfPages();
|
||||||
|
List<Integer> newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
|
||||||
|
|
||||||
|
// Create a new list to hold the pages in the new order
|
||||||
|
List<PDPage> newPages = new ArrayList<>();
|
||||||
|
for (int pageIndex : newPageOrder) {
|
||||||
|
newPages.add(document.getPage(pageIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all the pages from the original document
|
||||||
|
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
|
||||||
|
document.removePage(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the pages in the new order
|
||||||
|
for (PDPage page : newPages) {
|
||||||
|
document.addPage(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert PDDocument to byte array
|
||||||
|
byte[] newPdfBytes;
|
||||||
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
||||||
|
document.save(baos);
|
||||||
|
newPdfBytes = baos.toByteArray();
|
||||||
|
} finally {
|
||||||
|
document.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPdfBytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
|
|
||||||
import jakarta.servlet.ServletContext;
|
import jakarta.servlet.ServletContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.SPdfApplication;
|
import stirling.software.SPDF.SPDFApplication;
|
||||||
import stirling.software.SPDF.model.ApiEndpoint;
|
import stirling.software.SPDF.model.ApiEndpoint;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ public class ApiDocService {
|
|||||||
|
|
||||||
private String getApiDocsUrl() {
|
private String getApiDocsUrl() {
|
||||||
String contextPath = servletContext.getContextPath();
|
String contextPath = servletContext.getContextPath();
|
||||||
String port = SPdfApplication.getStaticPort();
|
String port = SPDFApplication.getStaticPort();
|
||||||
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
return "http://localhost:" + port + contextPath + "/v1/api-docs";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -35,22 +34,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||||
public class PipelineController {
|
public class PipelineController {
|
||||||
|
|
||||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
|
||||||
|
|
||||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
|
||||||
|
|
||||||
private final PipelineProcessor processor;
|
private final PipelineProcessor processor;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public PipelineController(
|
public PipelineController(PipelineProcessor processor, ObjectMapper objectMapper) {
|
||||||
PipelineProcessor processor,
|
|
||||||
ApplicationProperties applicationProperties,
|
|
||||||
ObjectMapper objectMapper) {
|
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
this.applicationProperties = applicationProperties;
|
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
@@ -25,6 +24,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
import stirling.software.SPDF.utils.FileMonitor;
|
import stirling.software.SPDF.utils.FileMonitor;
|
||||||
@@ -48,14 +48,12 @@ public class PipelineDirectoryProcessor {
|
|||||||
public PipelineDirectoryProcessor(
|
public PipelineDirectoryProcessor(
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
ApiDocService apiDocService,
|
ApiDocService apiDocService,
|
||||||
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
|
|
||||||
@Qualifier("finishedFoldersDir") String finishedFoldersDir,
|
|
||||||
PipelineProcessor processor,
|
PipelineProcessor processor,
|
||||||
FileMonitor fileMonitor) {
|
FileMonitor fileMonitor) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.apiDocService = apiDocService;
|
this.apiDocService = apiDocService;
|
||||||
this.watchedFoldersDir = watchedFoldersDir;
|
this.watchedFoldersDir = InstallationPathConfig.getPipelineWatchedFoldersDir();
|
||||||
this.finishedFoldersDir = finishedFoldersDir;
|
this.finishedFoldersDir = InstallationPathConfig.getPipelineFinishedFoldersDir();
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
this.fileMonitor = fileMonitor;
|
this.fileMonitor = fileMonitor;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import io.github.pixee.security.ZipSecurity;
|
|||||||
|
|
||||||
import jakarta.servlet.ServletContext;
|
import jakarta.servlet.ServletContext;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.SPdfApplication;
|
import stirling.software.SPDF.SPDFApplication;
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
import stirling.software.SPDF.model.PipelineOperation;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
@@ -80,7 +80,7 @@ public class PipelineProcessor {
|
|||||||
|
|
||||||
private String getBaseUrl() {
|
private String getBaseUrl() {
|
||||||
String contextPath = servletContext.getContextPath();
|
String contextPath = servletContext.getContextPath();
|
||||||
String port = SPdfApplication.getStaticPort();
|
String port = SPDFApplication.getStaticPort();
|
||||||
return "http://localhost:" + port + contextPath + "/";
|
return "http://localhost:" + port + contextPath + "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,18 @@ package stirling.software.SPDF.controller.api.security;
|
|||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
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.PDPageTree;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -22,11 +27,15 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
|
import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
|
import stirling.software.SPDF.model.api.security.RedactionArea;
|
||||||
import stirling.software.SPDF.pdf.TextFinder;
|
import stirling.software.SPDF.pdf.TextFinder;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
import stirling.software.SPDF.utils.propertyeditor.StringToArrayListPropertyEditor;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/security")
|
@RequestMapping("/api/v1/security")
|
||||||
@@ -41,6 +50,120 @@ public class RedactController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitBinder
|
||||||
|
public void initBinder(WebDataBinder binder) {
|
||||||
|
binder.registerCustomEditor(
|
||||||
|
List.class, "redactions", new StringToArrayListPropertyEditor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/redact", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Redacts areas and pages in a PDF document",
|
||||||
|
description =
|
||||||
|
"This operation takes an input PDF file with a list of areas, page number(s)/range(s)/function(s) to redact. Input:PDF, Output:PDF, Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> redactPDF(@ModelAttribute ManualRedactPdfRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
List<RedactionArea> redactionAreas = request.getRedactions();
|
||||||
|
|
||||||
|
PDDocument document = pdfDocumentFactory.load(file);
|
||||||
|
|
||||||
|
PDPageTree allPages = document.getDocumentCatalog().getPages();
|
||||||
|
|
||||||
|
redactPages(request, document, allPages);
|
||||||
|
redactAreas(redactionAreas, document, allPages);
|
||||||
|
|
||||||
|
if (request.isConvertPDFToImage()) {
|
||||||
|
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
|
||||||
|
document.close();
|
||||||
|
document = convertedPdf;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
document.save(baos);
|
||||||
|
document.close();
|
||||||
|
|
||||||
|
byte[] pdfContent = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
pdfContent,
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_redacted.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redactAreas(
|
||||||
|
List<RedactionArea> redactionAreas, PDDocument document, PDPageTree allPages)
|
||||||
|
throws IOException {
|
||||||
|
Color redactColor = null;
|
||||||
|
for (RedactionArea redactionArea : redactionAreas) {
|
||||||
|
if (redactionArea.getPage() == null
|
||||||
|
|| redactionArea.getPage() <= 0
|
||||||
|
|| redactionArea.getHeight() == null
|
||||||
|
|| redactionArea.getHeight() <= 0.0D
|
||||||
|
|| redactionArea.getWidth() == null
|
||||||
|
|| redactionArea.getWidth() <= 0.0D) continue;
|
||||||
|
PDPage page = allPages.get(redactionArea.getPage() - 1);
|
||||||
|
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
|
redactColor = decodeOrDefault(redactionArea.getColor(), Color.BLACK);
|
||||||
|
contentStream.setNonStrokingColor(redactColor);
|
||||||
|
|
||||||
|
float x = redactionArea.getX().floatValue();
|
||||||
|
float y = redactionArea.getY().floatValue();
|
||||||
|
float width = redactionArea.getWidth().floatValue();
|
||||||
|
float height = redactionArea.getHeight().floatValue();
|
||||||
|
|
||||||
|
PDRectangle box = page.getBBox();
|
||||||
|
|
||||||
|
contentStream.addRect(x, box.getHeight() - y - height, width, height);
|
||||||
|
contentStream.fill();
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void redactPages(
|
||||||
|
ManualRedactPdfRequest request, PDDocument document, PDPageTree allPages)
|
||||||
|
throws IOException {
|
||||||
|
Color redactColor = decodeOrDefault(request.getPageRedactionColor(), Color.BLACK);
|
||||||
|
List<Integer> pageNumbers = getPageNumbers(request, allPages.getCount());
|
||||||
|
for (Integer pageNumber : pageNumbers) {
|
||||||
|
PDPage page = allPages.get(pageNumber);
|
||||||
|
|
||||||
|
PDPageContentStream contentStream =
|
||||||
|
new PDPageContentStream(
|
||||||
|
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
|
contentStream.setNonStrokingColor(redactColor);
|
||||||
|
|
||||||
|
PDRectangle box = page.getBBox();
|
||||||
|
|
||||||
|
contentStream.addRect(0, 0, box.getWidth(), box.getHeight());
|
||||||
|
contentStream.fill();
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Color decodeOrDefault(String hex, Color defaultColor) {
|
||||||
|
Color color = null;
|
||||||
|
try {
|
||||||
|
color = Color.decode(hex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
color = defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> getPageNumbers(ManualRedactPdfRequest request, int pagesCount) {
|
||||||
|
String pageNumbersInput = request.getPageNumbers();
|
||||||
|
String[] parsedPageNumbers =
|
||||||
|
pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0];
|
||||||
|
List<Integer> pageNumbers =
|
||||||
|
GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
|
||||||
|
Collections.sort(pageNumbers);
|
||||||
|
return pageNumbers;
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Redacts listOfText in a PDF document",
|
summary = "Redacts listOfText in a PDF document",
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
import org.bouncycastle.cert.X509CertificateHolder;
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
import org.bouncycastle.cms.*;
|
import org.bouncycastle.cms.CMSProcessable;
|
||||||
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.SignerInformation;
|
||||||
|
import org.bouncycastle.cms.SignerInformationStore;
|
||||||
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||||
import org.bouncycastle.util.Store;
|
import org.bouncycastle.util.Store;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|||||||
@@ -163,6 +163,9 @@ public class AccountWebController {
|
|||||||
case "invalid_destination":
|
case "invalid_destination":
|
||||||
erroroauth = "login.invalid_destination";
|
erroroauth = "login.invalid_destination";
|
||||||
break;
|
break;
|
||||||
|
case "relying_party_registration_not_found":
|
||||||
|
erroroauth = "login.relyingPartyRegistrationNotFound";
|
||||||
|
break;
|
||||||
// Valid InResponseTo was not available from the validation context, unable to
|
// Valid InResponseTo was not available from the validation context, unable to
|
||||||
// evaluate
|
// evaluate
|
||||||
case "invalid_in_response_to":
|
case "invalid_in_response_to":
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
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 stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
import stirling.software.SPDF.config.security.database.DatabaseService;
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "Database Management", description = "Database management and security APIs")
|
@Tag(name = "Database Management", description = "Database management and security APIs")
|
||||||
public class DatabaseWebController {
|
public class DatabaseWebController {
|
||||||
|
|
||||||
private final DatabaseBackupHelper databaseBackupHelper;
|
private final DatabaseService databaseService;
|
||||||
|
|
||||||
public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) {
|
public DatabaseWebController(DatabaseService databaseService) {
|
||||||
this.databaseBackupHelper = databaseBackupHelper;
|
this.databaseService = databaseService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||||
@@ -34,9 +34,12 @@ public class DatabaseWebController {
|
|||||||
} else if (confirmed != null) {
|
} else if (confirmed != null) {
|
||||||
model.addAttribute("infoMessage", confirmed);
|
model.addAttribute("infoMessage", confirmed);
|
||||||
}
|
}
|
||||||
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
List<FileInfo> backupList = databaseService.getBackupList();
|
||||||
model.addAttribute("backupFiles", backupList);
|
model.addAttribute("backupFiles", backupList);
|
||||||
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
|
model.addAttribute("databaseVersion", databaseService.getH2Version());
|
||||||
|
if ("Unknown".equalsIgnoreCase(databaseService.getH2Version())) {
|
||||||
|
model.addAttribute("infoMessage", "notSupported");
|
||||||
|
}
|
||||||
return "database";
|
return "database";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Hidden;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.SignatureFile;
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
import stirling.software.SPDF.service.SignatureService;
|
import stirling.software.SPDF.service.SignatureService;
|
||||||
@@ -34,8 +35,6 @@ import stirling.software.SPDF.service.SignatureService;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class GeneralWebController {
|
public class GeneralWebController {
|
||||||
|
|
||||||
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
|
|
||||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
|
||||||
private final SignatureService signatureService;
|
private final SignatureService signatureService;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
@@ -223,7 +222,9 @@ public class GeneralWebController {
|
|||||||
// Extract font names from classpath
|
// Extract font names from classpath
|
||||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||||
// Extract font names from external directory
|
// Extract font names from external directory
|
||||||
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
|
fontNames.addAll(
|
||||||
|
getFontNamesFromLocation(
|
||||||
|
"file:" + InstallationPathConfig.getStaticPath() + "fonts/*"));
|
||||||
return fontNames;
|
return fontNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ public class SecurityWebController {
|
|||||||
return "security/auto-redact";
|
return "security/auto-redact";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/redact")
|
||||||
|
public String redactForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "redact");
|
||||||
|
return "security/redact";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/add-password")
|
@GetMapping("/add-password")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String addPasswordForm(Model model) {
|
public String addPasswordForm(Model model) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package stirling.software.SPDF.model;
|
package stirling.software.SPDF.model;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
@@ -13,18 +15,23 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.core.env.ConfigurableEnvironment;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
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;
|
||||||
@@ -33,11 +40,37 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "")
|
@ConfigurationProperties(prefix = "")
|
||||||
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
|
||||||
@Data
|
@Data
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
@Slf4j
|
||||||
public class ApplicationProperties {
|
public class ApplicationProperties {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||||
|
throws IOException {
|
||||||
|
String configPath = InstallationPathConfig.getSettingsPath();
|
||||||
|
log.debug("Attempting to load settings from: " + configPath);
|
||||||
|
|
||||||
|
File file = new File(configPath);
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.error("Warning: Settings file does not exist at: " + configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = new FileSystemResource(configPath);
|
||||||
|
if (!resource.exists()) {
|
||||||
|
throw new FileNotFoundException("Settings file not found at: " + configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
EncodedResource encodedResource = new EncodedResource(resource);
|
||||||
|
PropertySource<?> propertySource =
|
||||||
|
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||||
|
environment.getPropertySources().addFirst(propertySource);
|
||||||
|
|
||||||
|
log.debug("Loaded properties: " + propertySource.getSource());
|
||||||
|
|
||||||
|
return propertySource;
|
||||||
|
}
|
||||||
|
|
||||||
private Legal legal = new Legal();
|
private Legal legal = new Legal();
|
||||||
private Security security = new Security();
|
private Security security = new Security();
|
||||||
private System system = new System();
|
private System system = new System();
|
||||||
@@ -79,23 +112,6 @@ public class ApplicationProperties {
|
|||||||
return saml2.getEnabled() || oauth2.getEnabled();
|
return saml2.getEnabled() || oauth2.getEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUserPass() {
|
|
||||||
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|
|
||||||
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOauth2Activ() {
|
|
||||||
return (oauth2 != null
|
|
||||||
&& oauth2.getEnabled()
|
|
||||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSaml2Activ() {
|
|
||||||
return (saml2 != null
|
|
||||||
&& saml2.getEnabled()
|
|
||||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LoginMethods {
|
public enum LoginMethods {
|
||||||
ALL("all"),
|
ALL("all"),
|
||||||
NORMAL("normal"),
|
NORMAL("normal"),
|
||||||
@@ -114,6 +130,23 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isUserPass() {
|
||||||
|
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|
||||||
|
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOauth2Activ() {
|
||||||
|
return (oauth2 != null
|
||||||
|
&& oauth2.getEnabled()
|
||||||
|
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSaml2Activ() {
|
||||||
|
return (saml2 != null
|
||||||
|
&& saml2.getEnabled()
|
||||||
|
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class InitialLogin {
|
public static class InitialLogin {
|
||||||
private String username;
|
private String username;
|
||||||
@@ -153,6 +186,7 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Resource getSpCert() {
|
public Resource getSpCert() {
|
||||||
|
if (spCert == null) return null;
|
||||||
if (spCert.startsWith("classpath:")) {
|
if (spCert.startsWith("classpath:")) {
|
||||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
return new ClassPathResource(spCert.substring("classpath:".length()));
|
||||||
} else {
|
} else {
|
||||||
@@ -161,6 +195,7 @@ public class ApplicationProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Resource getidpCert() {
|
public Resource getidpCert() {
|
||||||
|
if (idpCert == null) return null;
|
||||||
if (idpCert.startsWith("classpath:")) {
|
if (idpCert.startsWith("classpath:")) {
|
||||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||||
} else {
|
} else {
|
||||||
@@ -247,6 +282,42 @@ public class ApplicationProperties {
|
|||||||
private String tessdataDir;
|
private String tessdataDir;
|
||||||
private Boolean enableAlphaFunctionality;
|
private Boolean enableAlphaFunctionality;
|
||||||
private String enableAnalytics;
|
private String enableAnalytics;
|
||||||
|
private Datasource datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class Datasource {
|
||||||
|
private boolean enableCustomDatabase;
|
||||||
|
private String customDatabaseUrl;
|
||||||
|
private String type;
|
||||||
|
private String hostName;
|
||||||
|
private Integer port;
|
||||||
|
private String name;
|
||||||
|
private String username;
|
||||||
|
@ToString.Exclude private String password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Driver {
|
||||||
|
H2("h2"),
|
||||||
|
POSTGRESQL("postgresql"),
|
||||||
|
ORACLE("oracle"),
|
||||||
|
MYSQL("mysql");
|
||||||
|
|
||||||
|
private final String driverName;
|
||||||
|
|
||||||
|
Driver(String driverName) {
|
||||||
|
this.driverName = driverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return """
|
||||||
|
Driver {
|
||||||
|
driverName='%s'
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
.formatted(driverName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -295,6 +366,7 @@ public class ApplicationProperties {
|
|||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
private int maxUsers;
|
private int maxUsers;
|
||||||
|
private boolean ssoAutoLogin;
|
||||||
private CustomMetadata customMetadata = new CustomMetadata();
|
private CustomMetadata customMetadata = new CustomMetadata();
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import java.util.Date;
|
|||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import jakarta.persistence.Lob;
|
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ import lombok.Data;
|
|||||||
public class SessionEntity implements Serializable {
|
public class SessionEntity implements Serializable {
|
||||||
@Id private String sessionId;
|
@Id private String sessionId;
|
||||||
|
|
||||||
@Lob private String principalName;
|
private String principalName;
|
||||||
|
|
||||||
private Date lastRequest;
|
private Date lastRequest;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class User implements Serializable {
|
|||||||
@ElementCollection
|
@ElementCollection
|
||||||
@MapKeyColumn(name = "setting_key")
|
@MapKeyColumn(name = "setting_key")
|
||||||
@Lob
|
@Lob
|
||||||
@Column(name = "setting_value", columnDefinition = "CLOB")
|
@Column(name = "setting_value", columnDefinition = "text")
|
||||||
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
|
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
|
||||||
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ public class ConvertToImageRequest extends PDFFile {
|
|||||||
allowableValues = {"single", "multiple"})
|
allowableValues = {"single", "multiple"})
|
||||||
private String singleOrMultiple;
|
private String singleOrMultiple;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"")
|
||||||
|
private String pageNumbers;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The color type of the output image(s)",
|
description = "The color type of the output image(s)",
|
||||||
allowableValues = {"color", "greyscale", "blackwhite"})
|
allowableValues = {"color", "greyscale", "blackwhite"})
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ManualRedactPdfRequest extends PDFWithPageNums {
|
||||||
|
@Schema(description = "A list of areas that should be redacted")
|
||||||
|
private List<RedactionArea> redactions;
|
||||||
|
|
||||||
|
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
||||||
|
private boolean convertPDFToImage;
|
||||||
|
|
||||||
|
@Schema(description = "The color used to fully redact certain pages")
|
||||||
|
private String pageRedactionColor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RedactionArea {
|
||||||
|
@Schema(description = "The left edge point of the area to be redacted.")
|
||||||
|
private Double x;
|
||||||
|
|
||||||
|
@Schema(description = "The top edge point of the area to be redacted.")
|
||||||
|
private Double y;
|
||||||
|
|
||||||
|
@Schema(description = "The height of the area to be redacted.")
|
||||||
|
private Double height;
|
||||||
|
|
||||||
|
@Schema(description = "The width of the area to be redacted.")
|
||||||
|
private Double width;
|
||||||
|
|
||||||
|
@Schema(description = "The page on which the area should be redacted.")
|
||||||
|
private Integer page;
|
||||||
|
|
||||||
|
@Schema(description = "The color used to redact the specified area.")
|
||||||
|
private String color;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package stirling.software.SPDF.model.exception;
|
||||||
|
|
||||||
|
public class BackupNotFoundException extends RuntimeException {
|
||||||
|
public BackupNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,14 +13,19 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.thymeleaf.util.StringUtils;
|
import org.thymeleaf.util.StringUtils;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.SignatureFile;
|
import stirling.software.SPDF.model.SignatureFile;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SignatureService {
|
public class SignatureService {
|
||||||
|
|
||||||
private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
|
private final String SIGNATURE_BASE_PATH;
|
||||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||||
|
|
||||||
|
public SignatureService() {
|
||||||
|
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||||
validateFileName(fileName);
|
validateFileName(fileName);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.scheduling.annotation.Scheduled;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -34,9 +35,7 @@ public class FileMonitor {
|
|||||||
* monitored, false otherwise
|
* monitored, false otherwise
|
||||||
*/
|
*/
|
||||||
@Autowired
|
@Autowired
|
||||||
public FileMonitor(
|
public FileMonitor(@Qualifier("directoryFilter") Predicate<Path> pathFilter)
|
||||||
@Qualifier("watchedFoldersDir") String rootDirectory,
|
|
||||||
@Qualifier("directoryFilter") Predicate<Path> pathFilter)
|
|
||||||
throws IOException {
|
throws IOException {
|
||||||
this.newlyDiscoveredFiles = new HashSet<>();
|
this.newlyDiscoveredFiles = new HashSet<>();
|
||||||
this.path2KeyMapping = new HashMap<>();
|
this.path2KeyMapping = new HashMap<>();
|
||||||
@@ -44,7 +43,7 @@ public class FileMonitor {
|
|||||||
this.pathFilter = pathFilter;
|
this.pathFilter = pathFilter;
|
||||||
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
||||||
this.watchService = FileSystems.getDefault().newWatchService();
|
this.watchService = FileSystems.getDefault().newWatchService();
|
||||||
this.rootDir = Path.of(rootDirectory);
|
this.rootDir = Path.of(InstallationPathConfig.getPipelineWatchedFoldersDir());
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldNotProcess(Path path) {
|
private boolean shouldNotProcess(Path path) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import io.github.pixee.security.HostValidator;
|
|||||||
import io.github.pixee.security.Urls;
|
import io.github.pixee.security.Urls;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class GeneralUtils {
|
public class GeneralUtils {
|
||||||
@@ -83,7 +84,7 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
// Allow only http and https protocols
|
// Allow only http and https protocols
|
||||||
String protocol = url.getProtocol();
|
String protocol = url.getProtocol();
|
||||||
if (!protocol.equals("http") && !protocol.equals("https")) {
|
if (!"http".equals(protocol) && !"https".equals(protocol)) {
|
||||||
return false; // Disallow other protocols
|
return false; // Disallow other protocols
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,32 +221,54 @@ public class GeneralUtils {
|
|||||||
throw new IllegalArgumentException("Invalid expression");
|
throw new IllegalArgumentException("Invalid expression");
|
||||||
}
|
}
|
||||||
|
|
||||||
int n = 0;
|
for (int n = 1; n <= maxValue; n++) {
|
||||||
while (true) {
|
|
||||||
// Replace 'n' with the current value of n, correctly handling numbers before
|
// Replace 'n' with the current value of n, correctly handling numbers before
|
||||||
// 'n'
|
// 'n'
|
||||||
String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
|
String sanitizedExpression = sanitizeNFunction(expression, n);
|
||||||
Double result = evaluator.evaluate(sanitizedExpression);
|
Double result = evaluator.evaluate(sanitizedExpression);
|
||||||
|
|
||||||
// Check if the result is null or not within bounds
|
// Check if the result is null or not within bounds
|
||||||
if (result == null || result <= 0 || result.intValue() > maxValue) {
|
if (result == null) break;
|
||||||
if (n != 0) break;
|
|
||||||
} else {
|
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
||||||
results.add(result.intValue());
|
results.add(result.intValue());
|
||||||
}
|
}
|
||||||
n++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String sanitizeNFunction(String expression, int nValue) {
|
||||||
|
String sanitizedExpression = expression.replace(" ", "");
|
||||||
|
String multiplyByOpeningRoundBracketPattern =
|
||||||
|
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
|
||||||
|
sanitizedExpression =
|
||||||
|
sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*(");
|
||||||
|
|
||||||
|
String multiplyByClosingRoundBracketPattern =
|
||||||
|
"\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2)
|
||||||
|
sanitizedExpression =
|
||||||
|
sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1");
|
||||||
|
|
||||||
|
sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue);
|
||||||
|
return sanitizedExpression;
|
||||||
|
}
|
||||||
|
|
||||||
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
private static String insertMultiplicationBeforeN(String expression, int nValue) {
|
||||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
||||||
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
||||||
|
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
||||||
// Now replace 'n' with its current value
|
// Now replace 'n' with its current value
|
||||||
return withMultiplication.replace("n", String.valueOf(nValue));
|
return withMultiplication.replace("n", String.valueOf(nValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String formatConsecutiveNsForNFunction(String expression) {
|
||||||
|
String text = expression;
|
||||||
|
while (text.matches(".*n{2,}.*")) {
|
||||||
|
text = text.replaceAll("(?<!n)n{2}", "n*n");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
private static List<Integer> handlePart(String part, int totalPages, int offset) {
|
||||||
List<Integer> partResult = new ArrayList<>();
|
List<Integer> partResult = new ArrayList<>();
|
||||||
|
|
||||||
@@ -320,7 +343,10 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path =
|
||||||
|
Paths.get(
|
||||||
|
InstallationPathConfig
|
||||||
|
.getSettingsPath()); // Target the configs/settings.yml
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
DumperOptions yamlOptionssettingsYml =
|
DumperOptions yamlOptionssettingsYml =
|
||||||
@@ -338,7 +364,7 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
|
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Path path = Paths.get("configs", "settings.yml");
|
Path path = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
DumperOptions yamlOptionssettingsYml =
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
package stirling.software.SPDF.utils;
|
|
||||||
|
|
||||||
public class PDFManipulationUtils {}
|
|
||||||
@@ -9,6 +9,7 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ public class PDFToFile {
|
|||||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
||||||
|
|
||||||
// Get output files
|
// Get output files
|
||||||
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
|
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
|
||||||
|
|
||||||
// Return output files in a ZIP archive
|
// Return output files in a ZIP archive
|
||||||
fileName = pdfBaseName + "ToHtml.zip";
|
fileName = pdfBaseName + "ToHtml.zip";
|
||||||
|
|||||||
@@ -22,9 +22,11 @@ import org.apache.pdfbox.text.TextPosition;
|
|||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
|
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
|
||||||
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
|
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||||
|
|
||||||
private String textColor;
|
private String textColor;
|
||||||
@@ -93,17 +95,17 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
|||||||
try {
|
try {
|
||||||
font = PDFontFactory.createFont(text.getFont().getCOSObject());
|
font = PDFontFactory.createFont(text.getFont().getCOSObject());
|
||||||
} catch (IOException io) {
|
} catch (IOException io) {
|
||||||
System.out.println("Primary font not found, using fallback font.");
|
log.info("Primary font not found, using fallback font.");
|
||||||
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||||
}
|
}
|
||||||
// if a character is not supported by font, then look for supported font
|
// if a character is not supported by font, then look for supported font
|
||||||
try {
|
try {
|
||||||
byte[] bytes = font.encode(unicodeText);
|
byte[] bytes = font.encode(unicodeText);
|
||||||
} catch (IOException io) {
|
} catch (IOException io) {
|
||||||
System.out.println("text could not be encoded ");
|
log.info("text could not be encoded ");
|
||||||
font = checkSupportedFontForCharacter(unicodeText);
|
font = checkSupportedFontForCharacter(unicodeText);
|
||||||
} catch (IllegalArgumentException ie) {
|
} catch (IllegalArgumentException ie) {
|
||||||
System.out.println("text not supported by font ");
|
log.info("text not supported by font ");
|
||||||
font = checkSupportedFontForCharacter(unicodeText);
|
font = checkSupportedFontForCharacter(unicodeText);
|
||||||
} finally {
|
} finally {
|
||||||
// if any other font is not supported, then replace default character *
|
// if any other font is not supported, then replace default character *
|
||||||
@@ -157,9 +159,9 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
|||||||
byte[] bytes = currentFont.encode(unicodeText);
|
byte[] bytes = currentFont.encode(unicodeText);
|
||||||
return currentFont;
|
return currentFont;
|
||||||
} catch (IOException io) {
|
} catch (IOException io) {
|
||||||
System.out.println("text could not be encoded ");
|
log.info("text could not be encoded ");
|
||||||
} catch (IllegalArgumentException ie) {
|
} catch (IllegalArgumentException ie) {
|
||||||
System.out.println("text not supported by font ");
|
log.info("text not supported by font ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package stirling.software.SPDF.utils.propertyeditor;
|
||||||
|
|
||||||
|
import java.beans.PropertyEditorSupport;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.api.security.RedactionArea;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class StringToArrayListPropertyEditor extends PropertyEditorSupport {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAsText(String text) throws IllegalArgumentException {
|
||||||
|
if (text == null || text.trim().isEmpty()) {
|
||||||
|
setValue(new ArrayList<>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||||
|
TypeReference<ArrayList<RedactionArea>> typeRef =
|
||||||
|
new TypeReference<ArrayList<RedactionArea>>() {};
|
||||||
|
List<RedactionArea> list = objectMapper.readValue(text, typeRef);
|
||||||
|
setValue(list);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Exception while converting {}", e);
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to convert java.lang.String to java.util.List");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,9 +27,8 @@ spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
|
|||||||
spring.thymeleaf.encoding=UTF-8
|
spring.thymeleaf.encoding=UTF-8
|
||||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||||
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
|
||||||
#spring.thymeleaf.cache=false
|
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
|
||||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
|
||||||
spring.datasource.driver-class-name=org.h2.Driver
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
|
<define name="LOG_PATH" class="stirling.software.SPDF.config.LogbackPropertyLoader" />
|
||||||
|
|
||||||
<!-- Console Appender -->
|
<!-- Console Appender -->
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
@@ -7,35 +9,30 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- Rolling File Appender -->
|
<!-- Rolling File Appender for Auth Logs -->
|
||||||
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>logs/invalid-auths.log</file>
|
<file>${LOG_PATH}/invalid-auths.log</file>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
|
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<!-- daily rollover and keep 7 days' worth of history -->
|
<fileNamePattern>${LOG_PATH}/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
<fileNamePattern>logs/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
|
|
||||||
<maxHistory>1</maxHistory>
|
<maxHistory>1</maxHistory>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<!-- Rolling File Appender -->
|
<!-- Rolling File Appender for General Logs -->
|
||||||
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
<file>logs/info.log</file>
|
<file>${LOG_PATH}/info.log</file>
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
|
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
<!-- daily rollover and keep 7 days' worth of history -->
|
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||||
<fileNamePattern>logs/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
|
||||||
<maxHistory>1</maxHistory>
|
<maxHistory>1</maxHistory>
|
||||||
</rollingPolicy>
|
</rollingPolicy>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
|
||||||
<!-- Root Logger -->
|
<!-- Root Logger -->
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
@@ -43,10 +40,9 @@
|
|||||||
</root>
|
</root>
|
||||||
|
|
||||||
<!-- Specific Logger -->
|
<!-- Specific Logger -->
|
||||||
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" level="ERROR"
|
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler"
|
||||||
additivity="false">
|
level="ERROR" additivity="false">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
<appender-ref ref="AUTHLOG"/>
|
<appender-ref ref="AUTHLOG"/>
|
||||||
</logger>
|
</logger>
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -82,6 +82,7 @@ pages=صفحات
|
|||||||
loading=جارٍ التحميل...
|
loading=جارٍ التحميل...
|
||||||
addToDoc=إضافة إلى المستند
|
addToDoc=إضافة إلى المستند
|
||||||
reset=إعداة ضبط
|
reset=إعداة ضبط
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=سياسة الخصوصية
|
legal.privacy=سياسة الخصوصية
|
||||||
legal.terms=شروط الاستخدام
|
legal.terms=شروط الاستخدام
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=لم يتم العثور على الملف
|
database.fileNotFound=لم يتم العثور على الملف
|
||||||
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
||||||
database.failedImportFile=فشل استيراد الملف
|
database.failedImportFile=فشل استيراد الملف
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
|
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
|
||||||
session.refreshPage=تحديث الصفحة
|
session.refreshPage=تحديث الصفحة
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=حجب تلقائي
|
|||||||
home.autoRedact.desc=يحجب (يسود) النص في PDF تلقائيًا بناءً على النص المدخل
|
home.autoRedact.desc=يحجب (يسود) النص في PDF تلقائيًا بناءً على النص المدخل
|
||||||
autoRedact.tags=حجب,إخفاء,تسويد,أسود,علامة,مخفي
|
autoRedact.tags=حجب,إخفاء,تسويد,أسود,علامة,مخفي
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF إلى CSV
|
home.tableExtraxt.title=PDF إلى CSV
|
||||||
home.tableExtraxt.desc=يستخرج الجداول من PDF ويحولها إلى CSV
|
home.tableExtraxt.desc=يستخرج الجداول من PDF ويحولها إلى CSV
|
||||||
tableExtraxt.tags=CSV,استخراج الجدول,استخراج,تحويل
|
tableExtraxt.tags=CSV,استخراج الجدول,استخراج,تحويل
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=طلب غير صالح
|
|||||||
login.oauth2AccessDenied=تم رفض الوصول
|
login.oauth2AccessDenied=تم رفض الوصول
|
||||||
login.oauth2InvalidTokenResponse=استجابة الرمز المميز غير صالحة
|
login.oauth2InvalidTokenResponse=استجابة الرمز المميز غير صالحة
|
||||||
login.oauth2InvalidIdToken=رمز الهوية غير صالح
|
login.oauth2InvalidIdToken=رمز الهوية غير صالح
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل الدخول حاليًا باستخدام اسم المستخدم هذا. يرجى الاتصال بالمسؤول.
|
login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل الدخول حاليًا باستخدام اسم المستخدم هذا. يرجى الاتصال بالمسؤول.
|
||||||
login.alreadyLoggedIn=لقد تسجل دخولًا إلى
|
login.alreadyLoggedIn=لقد تسجل دخولًا إلى
|
||||||
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
|
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=حشو إضافي مخصص
|
|||||||
autoRedact.convertPDFToImageLabel=تحويل PDF إلى صورة PDF (يستخدم لإزالة النص خلف المربع)
|
autoRedact.convertPDFToImageLabel=تحويل PDF إلى صورة PDF (يستخدم لإزالة النص خلف المربع)
|
||||||
autoRedact.submitButton=إرسال
|
autoRedact.submitButton=إرسال
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=إظهار جافا سكريبت
|
showJS.title=إظهار جافا سكريبت
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=إصلاح
|
repair.title=إصلاح
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=تدرج الرمادي
|
|||||||
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
|
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
|
||||||
pdfToImage.submit=تحويل
|
pdfToImage.submit=تحويل
|
||||||
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
|
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
|
||||||
|
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=تقطيع ملف PDF
|
|||||||
fileChooser.click=انقر هنا
|
fileChooser.click=انقر هنا
|
||||||
fileChooser.or=أو
|
fileChooser.or=أو
|
||||||
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
|
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Səhifələr
|
|||||||
loading=Yüklənir...
|
loading=Yüklənir...
|
||||||
addToDoc=Sənədə Əlavə Et
|
addToDoc=Sənədə Əlavə Et
|
||||||
reset=Sıfırla
|
reset=Sıfırla
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Məxfilik Siyasəti
|
legal.privacy=Məxfilik Siyasəti
|
||||||
legal.terms=Qaydalar və Şərtlər
|
legal.terms=Qaydalar və Şərtlər
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=Fayl Tapılmadı
|
database.fileNotFound=Fayl Tapılmadı
|
||||||
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
|
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
|
||||||
database.failedImportFile=Faylı daxil etmək alınmadı
|
database.failedImportFile=Faylı daxil etmək alınmadı
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin.
|
session.expired=Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin.
|
||||||
session.refreshPage=Səhifəni Yenilə
|
session.refreshPage=Səhifəni Yenilə
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Avtomatik Gizlətmə
|
|||||||
home.autoRedact.desc=Daxil edilmiş data əsasında PDF-dəki müəyyən mətn hissəsini qara qutu ilə gizlədir
|
home.autoRedact.desc=Daxil edilmiş data əsasında PDF-dəki müəyyən mətn hissəsini qara qutu ilə gizlədir
|
||||||
autoRedact.tags=Qarala,gizlət,sil,qara,marker,gizli
|
autoRedact.tags=Qarala,gizlət,sil,qara,marker,gizli
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF-dən CSV-ə
|
home.tableExtraxt.title=PDF-dən CSV-ə
|
||||||
home.tableExtraxt.desc=PDF-dən cədvəlləri CSV-ə çevirərək xaric edir
|
home.tableExtraxt.desc=PDF-dən cədvəlləri CSV-ə çevirərək xaric edir
|
||||||
tableExtraxt.tags=CSV,Cədvəl xaricetmə,xaric et,çevir
|
tableExtraxt.tags=CSV,Cədvəl xaricetmə,xaric et,çevir
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Etibarsız Sorğu
|
|||||||
login.oauth2AccessDenied=Giriş rədd edildi
|
login.oauth2AccessDenied=Giriş rədd edildi
|
||||||
login.oauth2InvalidTokenResponse=Etibarsız Token Cavabı
|
login.oauth2InvalidTokenResponse=Etibarsız Token Cavabı
|
||||||
login.oauth2InvalidIdToken=Etibarsız Id Token
|
login.oauth2InvalidIdToken=Etibarsız Id Token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=İstifadəçi deaktivləşdirilmişdir, bu istifadəçi adı ilə giriş hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
|
login.userIsDisabled=İstifadəçi deaktivləşdirilmişdir, bu istifadəçi adı ilə giriş hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
|
||||||
login.alreadyLoggedIn=Siz artıq daxil olmusunuz
|
login.alreadyLoggedIn=Siz artıq daxil olmusunuz
|
||||||
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
|
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Fərdi Əlavə Başlıq
|
|||||||
autoRedact.convertPDFToImageLabel=PDF-i PDF-Şəkil-ə çevir (Qutunun arxasındakı yazını silmək üçün istifadə edilir)
|
autoRedact.convertPDFToImageLabel=PDF-i PDF-Şəkil-ə çevir (Qutunun arxasındakı yazını silmək üçün istifadə edilir)
|
||||||
autoRedact.submitButton=Təsdiqlə
|
autoRedact.submitButton=Təsdiqlə
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Javascripti Göstər
|
showJS.title=Javascripti Göstər
|
||||||
@@ -830,6 +862,8 @@ sign.first=İlk səhifə
|
|||||||
sign.last=Son səhifə
|
sign.last=Son səhifə
|
||||||
sign.next=Növbəti səhifə
|
sign.next=Növbəti səhifə
|
||||||
sign.previous=Əvvəlki səhifə
|
sign.previous=Əvvəlki səhifə
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Bərpa Et
|
repair.title=Bərpa Et
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Boz Tonlama
|
|||||||
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
|
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
|
||||||
pdfToImage.submit=Çevir
|
pdfToImage.submit=Çevir
|
||||||
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
|
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
|
||||||
|
pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=PDF-i Ayır
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Страници
|
|||||||
loading=Loading...
|
loading=Loading...
|
||||||
addToDoc=Add to Document
|
addToDoc=Add to Document
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Политика за поверителност
|
legal.privacy=Политика за поверителност
|
||||||
legal.terms=Правила и условия
|
legal.terms=Правила и условия
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=Файлът не е намерен
|
database.fileNotFound=Файлът не е намерен
|
||||||
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
|
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
|
||||||
database.failedImportFile=Неуспешно импортиране на файл
|
database.failedImportFile=Неуспешно импортиране на файл
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
|
session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Автоматично редактиране
|
|||||||
home.autoRedact.desc=Автоматично редактира (зачернява) текст в PDF въз основа на въведен текст
|
home.autoRedact.desc=Автоматично редактира (зачернява) текст в PDF въз основа на въведен текст
|
||||||
autoRedact.tags=Редактиране,Скриване,затъмняване,черен,маркер,скрит
|
autoRedact.tags=Редактиране,Скриване,затъмняване,черен,маркер,скрит
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF в CSV
|
home.tableExtraxt.title=PDF в CSV
|
||||||
home.tableExtraxt.desc=Извлича таблици от PDF, като ги конвертира в CSV
|
home.tableExtraxt.desc=Извлича таблици от PDF, като ги конвертира в CSV
|
||||||
tableExtraxt.tags=CSV,извличане на таблица,извличане,конвертиране
|
tableExtraxt.tags=CSV,извличане на таблица,извличане,конвертиране
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Невалидна заявка
|
|||||||
login.oauth2AccessDenied=Отказан достъп
|
login.oauth2AccessDenied=Отказан достъп
|
||||||
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
|
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
|
||||||
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
|
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
|
login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
|
||||||
login.alreadyLoggedIn=Вече сте влезли в
|
login.alreadyLoggedIn=Вече сте влезли в
|
||||||
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
|
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Персонализирана допълните
|
|||||||
autoRedact.convertPDFToImageLabel=Преобразуване на PDF към PDF-изображение (използва се за премахване на текст зад полето)
|
autoRedact.convertPDFToImageLabel=Преобразуване на PDF към PDF-изображение (използва се за премахване на текст зад полето)
|
||||||
autoRedact.submitButton=Изпращане
|
autoRedact.submitButton=Изпращане
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Покажи Javascript
|
showJS.title=Покажи Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Поправи
|
repair.title=Поправи
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Скала на сивото
|
|||||||
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
|
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
|
||||||
pdfToImage.submit=Преобразуване
|
pdfToImage.submit=Преобразуване
|
||||||
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
|
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
|
||||||
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Разделяне на PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Pàgines
|
|||||||
loading=Carregant...
|
loading=Carregant...
|
||||||
addToDoc=Afegeix al document
|
addToDoc=Afegeix al document
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Política de Privacitat
|
legal.privacy=Política de Privacitat
|
||||||
legal.terms=Termes i condicions
|
legal.terms=Termes i condicions
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=Fitxer no trobat
|
database.fileNotFound=Fitxer no trobat
|
||||||
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
|
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
|
||||||
database.failedImportFile=Error en la importació del fitxer
|
database.failedImportFile=Error en la importació del fitxer
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho.
|
session.expired=La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho.
|
||||||
session.refreshPage=Actualitza la pàgina
|
session.refreshPage=Actualitza la pàgina
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Redacció Automàtica
|
|||||||
home.autoRedact.desc=Redacta automàticament (enfosqueix) text en un PDF basat en el text introduït
|
home.autoRedact.desc=Redacta automàticament (enfosqueix) text en un PDF basat en el text introduït
|
||||||
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF a CSV
|
home.tableExtraxt.title=PDF a CSV
|
||||||
home.tableExtraxt.desc=Extreu taules d'un PDF convertint-les a CSV
|
home.tableExtraxt.desc=Extreu taules d'un PDF convertint-les a CSV
|
||||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Sol·licitud no vàlida
|
|||||||
login.oauth2AccessDenied=Accés denegat
|
login.oauth2AccessDenied=Accés denegat
|
||||||
login.oauth2InvalidTokenResponse=Resposta de token no vàlida
|
login.oauth2InvalidTokenResponse=Resposta de token no vàlida
|
||||||
login.oauth2InvalidIdToken=ID Token no vàlid
|
login.oauth2InvalidIdToken=ID Token no vàlid
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=L'usuari està desactivat, l'inici de sessió està actualment bloquejat amb aquest nom d'usuari. Si us plau, contacta amb l'administrador.
|
login.userIsDisabled=L'usuari està desactivat, l'inici de sessió està actualment bloquejat amb aquest nom d'usuari. Si us plau, contacta amb l'administrador.
|
||||||
login.alreadyLoggedIn=Ja has iniciat sessió a
|
login.alreadyLoggedIn=Ja has iniciat sessió a
|
||||||
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
|
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Espai Extra Personalitzat
|
|||||||
autoRedact.convertPDFToImageLabel=Converteix PDF a Imatge PDF (S'utilitza per eliminar text darrere del quadre)
|
autoRedact.convertPDFToImageLabel=Converteix PDF a Imatge PDF (S'utilitza per eliminar text darrere del quadre)
|
||||||
autoRedact.submitButton=Envia
|
autoRedact.submitButton=Envia
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Mostra Javascript
|
showJS.title=Mostra Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparar
|
repair.title=Reparar
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Escala de Grisos
|
|||||||
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
|
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
|
||||||
pdfToImage.submit=Converteix
|
pdfToImage.submit=Converteix
|
||||||
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
|
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
|
||||||
|
pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Divideix PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Strany
|
|||||||
loading=Načítání...
|
loading=Načítání...
|
||||||
addToDoc=Přidat do dokumentu
|
addToDoc=Přidat do dokumentu
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Politika soukromí
|
legal.privacy=Politika soukromí
|
||||||
legal.terms=Podmínky použití
|
legal.terms=Podmínky použití
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=File not Found
|
database.fileNotFound=File not Found
|
||||||
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
|
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Failed Import File
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Vaše sesace vypršela. Prosím obnovte stránku a zkusit to znovu.
|
session.expired=Vaše sesace vypršela. Prosím obnovte stránku a zkusit to znovu.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Automatické odstranění
|
|||||||
home.autoRedact.desc=Automaticky zakrývá text v PDF na základě vstupního textu
|
home.autoRedact.desc=Automaticky zakrývá text v PDF na základě vstupního textu
|
||||||
autoRedact.tags=Odstranit,Skrytý,černý,zakrýt,značka,skrytý
|
autoRedact.tags=Odstranit,Skrytý,černý,zakrýt,značka,skrytý
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF na CSV
|
home.tableExtraxt.title=PDF na CSV
|
||||||
home.tableExtraxt.desc=Extrahuje tabulky z PDF a konvertuje je do formátu CSV
|
home.tableExtraxt.desc=Extrahuje tabulky z PDF a konvertuje je do formátu CSV
|
||||||
tableExtraxt.tags=CSV,Extrakce tabulky,extrahovat,konvertovat
|
tableExtraxt.tags=CSV,Extrakce tabulky,extrahovat,konvertovat
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Neplatný požadavek
|
|||||||
login.oauth2AccessDenied=Přístup zazobán
|
login.oauth2AccessDenied=Přístup zazobán
|
||||||
login.oauth2InvalidTokenResponse=Neplatná odpověď tokenu
|
login.oauth2InvalidTokenResponse=Neplatná odpověď tokenu
|
||||||
login.oauth2InvalidIdToken=Neplatný identifikační token
|
login.oauth2InvalidIdToken=Neplatný identifikační token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=Uživatel je deaktivován, přihlášení aktuálně pro tuto uživatelskou jmena je zakázáno. Kontaktujte správce.
|
login.userIsDisabled=Uživatel je deaktivován, přihlášení aktuálně pro tuto uživatelskou jmena je zakázáno. Kontaktujte správce.
|
||||||
login.alreadyLoggedIn=Jste již přihlášeni na
|
login.alreadyLoggedIn=Jste již přihlášeni na
|
||||||
login.alreadyLoggedIn2=zariadení. Odhlašujte se z těchto zařízení a zkuste to znovu.
|
login.alreadyLoggedIn2=zariadení. Odhlašujte se z těchto zařízení a zkuste to znovu.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Vlastní doplňující vzdálenost
|
|||||||
autoRedact.convertPDFToImageLabel=Převést PDF do PDF-Obrázku (Pro odstranění textu za obdélníkem)
|
autoRedact.convertPDFToImageLabel=Převést PDF do PDF-Obrázku (Pro odstranění textu za obdélníkem)
|
||||||
autoRedact.submitButton=Odeslat
|
autoRedact.submitButton=Odeslat
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Zobrazit JavaScript
|
showJS.title=Zobrazit JavaScript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Opravit
|
repair.title=Opravit
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Stupně šedi
|
|||||||
pdfToImage.blackwhite=Černobílý (Může dojít k ztrátě dat!)
|
pdfToImage.blackwhite=Černobílý (Může dojít k ztrátě dat!)
|
||||||
pdfToImage.submit=Převést
|
pdfToImage.submit=Převést
|
||||||
pdfToImage.info=Python není nainstalován. Potřebuje se pro konverzi do WebP.
|
pdfToImage.info=Python není nainstalován. Potřebuje se pro konverzi do WebP.
|
||||||
|
pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Podělit se PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Sideantal
|
|||||||
loading=Laster...
|
loading=Laster...
|
||||||
addToDoc=Tilføj til Dokument
|
addToDoc=Tilføj til Dokument
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Vilkår og betingelser
|
legal.terms=Vilkår og betingelser
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=Fil ikke fundet
|
database.fileNotFound=Fil ikke fundet
|
||||||
database.fileNullOrEmpty=Fil må ikke være null eller tom
|
database.fileNullOrEmpty=Fil må ikke være null eller tom
|
||||||
database.failedImportFile=Kunne ikke importere fil
|
database.failedImportFile=Kunne ikke importere fil
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Din sesions tid har udløbet. Genlad siden og prøv igen.
|
session.expired=Din sesions tid har udløbet. Genlad siden og prøv igen.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Rediger
|
|||||||
home.autoRedact.desc=Auto Redigerer (Sværter) tekst i en PDF baseret på input tekst
|
home.autoRedact.desc=Auto Redigerer (Sværter) tekst i en PDF baseret på input tekst
|
||||||
autoRedact.tags=Rediger,Skjul,sværte,sort,markør,skjult
|
autoRedact.tags=Rediger,Skjul,sværte,sort,markør,skjult
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF til CSV
|
home.tableExtraxt.title=PDF til CSV
|
||||||
home.tableExtraxt.desc=Udtrækker Tabeller fra en PDF og konverterer dem til CSV
|
home.tableExtraxt.desc=Udtrækker Tabeller fra en PDF og konverterer dem til CSV
|
||||||
tableExtraxt.tags=CSV,Tabeludtrækning,udtræk,konvertér
|
tableExtraxt.tags=CSV,Tabeludtrækning,udtræk,konvertér
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Ugyldig Anmodning
|
|||||||
login.oauth2AccessDenied=Adgang Nægtet
|
login.oauth2AccessDenied=Adgang Nægtet
|
||||||
login.oauth2InvalidTokenResponse=Ugyldigt Token Svar
|
login.oauth2InvalidTokenResponse=Ugyldigt Token Svar
|
||||||
login.oauth2InvalidIdToken=Ugyldigt Id Token
|
login.oauth2InvalidIdToken=Ugyldigt Id Token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med dette brugernavn. Kontakt venligst administratoren.
|
login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med dette brugernavn. Kontakt venligst administratoren.
|
||||||
login.alreadyLoggedIn=Du er allerede logget ind på
|
login.alreadyLoggedIn=Du er allerede logget ind på
|
||||||
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
|
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Brugerdefineret Ekstra Polstring
|
|||||||
autoRedact.convertPDFToImageLabel=Konvertér PDF til PDF-Billede (Bruges til at fjerne tekst bag boksen)
|
autoRedact.convertPDFToImageLabel=Konvertér PDF til PDF-Billede (Bruges til at fjerne tekst bag boksen)
|
||||||
autoRedact.submitButton=Indsend
|
autoRedact.submitButton=Indsend
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Vis Javascript
|
showJS.title=Vis Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparér
|
repair.title=Reparér
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Gråtone
|
|||||||
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
|
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
|
||||||
pdfToImage.submit=Konvertér
|
pdfToImage.submit=Konvertér
|
||||||
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
|
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
|
||||||
|
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Splitter PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Seiten
|
|||||||
loading=Laden...
|
loading=Laden...
|
||||||
addToDoc=In Dokument hinzufügen
|
addToDoc=In Dokument hinzufügen
|
||||||
reset=Zurücksetzen
|
reset=Zurücksetzen
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Datenschutz
|
legal.privacy=Datenschutz
|
||||||
legal.terms=AGB
|
legal.terms=AGB
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Datenbanksicherung erfolgreich
|
|||||||
database.fileNotFound=Datei nicht gefunden
|
database.fileNotFound=Datei nicht gefunden
|
||||||
database.fileNullOrEmpty=Datei darf nicht null oder leer sein
|
database.fileNullOrEmpty=Datei darf nicht null oder leer sein
|
||||||
database.failedImportFile=Dateiimport fehlgeschlagen
|
database.failedImportFile=Dateiimport fehlgeschlagen
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
||||||
session.refreshPage=Seite aktualisieren
|
session.refreshPage=Seite aktualisieren
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Automatisch zensieren/schwärzen
|
|||||||
home.autoRedact.desc=Automatisches Zensieren (Schwärzen) von Text in einer PDF-Datei basierend auf dem eingegebenen Text
|
home.autoRedact.desc=Automatisches Zensieren (Schwärzen) von Text in einer PDF-Datei basierend auf dem eingegebenen Text
|
||||||
autoRedact.tags=zensieren,schwärzen
|
autoRedact.tags=zensieren,schwärzen
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=Tabelle extrahieren
|
home.tableExtraxt.title=Tabelle extrahieren
|
||||||
home.tableExtraxt.desc=Tabelle aus PDF in CSV extrahieren
|
home.tableExtraxt.desc=Tabelle aus PDF in CSV extrahieren
|
||||||
tableExtraxt.tags=CSV,tabelle,extrahieren
|
tableExtraxt.tags=CSV,tabelle,extrahieren
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=ungültige Anfrage
|
|||||||
login.oauth2AccessDenied=Zugriff abgelehnt
|
login.oauth2AccessDenied=Zugriff abgelehnt
|
||||||
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
|
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
|
||||||
login.oauth2InvalidIdToken=Ungültiges ID-Token
|
login.oauth2InvalidIdToken=Ungültiges ID-Token
|
||||||
|
login.relyingPartyRegistrationNotFound=Keine Relying-Party-Registrierung gefunden
|
||||||
login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benutzernamen derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benutzernamen derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
||||||
login.alreadyLoggedIn=Sie sind bereits an
|
login.alreadyLoggedIn=Sie sind bereits an
|
||||||
login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut.
|
login.alreadyLoggedIn2=Geräten angemeldet. Bitte melden Sie sich dort ab und versuchen es dann erneut.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Zensierten Bereich vergrößern
|
|||||||
autoRedact.convertPDFToImageLabel=PDF in PDF-Bild konvertieren (zum Entfernen von Text hinter dem Kasten)
|
autoRedact.convertPDFToImageLabel=PDF in PDF-Bild konvertieren (zum Entfernen von Text hinter dem Kasten)
|
||||||
autoRedact.submitButton=Zensieren
|
autoRedact.submitButton=Zensieren
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Javascript anzeigen
|
showJS.title=Javascript anzeigen
|
||||||
@@ -830,6 +862,8 @@ sign.first=Erste Seite
|
|||||||
sign.last=Letzte Seite
|
sign.last=Letzte Seite
|
||||||
sign.next=Nächste Seite
|
sign.next=Nächste Seite
|
||||||
sign.previous=Vorherige Seite
|
sign.previous=Vorherige Seite
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparieren
|
repair.title=Reparieren
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Graustufen
|
|||||||
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
|
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
|
||||||
pdfToImage.submit=Umwandeln
|
pdfToImage.submit=Umwandeln
|
||||||
pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung.
|
pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung.
|
||||||
|
pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=PDF teilen
|
|||||||
fileChooser.click=Klicken
|
fileChooser.click=Klicken
|
||||||
fileChooser.or=oder
|
fileChooser.or=oder
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
|
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Σελίδες
|
|||||||
loading=Φόρτωση...
|
loading=Φόρτωση...
|
||||||
addToDoc=Πρόσθεση στο Εκπομπώματο
|
addToDoc=Πρόσθεση στο Εκπομπώματο
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Πολιτική Προνομίους
|
legal.privacy=Πολιτική Προνομίους
|
||||||
legal.terms=Φράσεις Υποχρεωτικότητας
|
legal.terms=Φράσεις Υποχρεωτικότητας
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=File not Found
|
database.fileNotFound=File not Found
|
||||||
database.fileNullOrEmpty=Το αρχείο δεν μπορεί να είναι τυχόν ή κενό.
|
database.fileNullOrEmpty=Το αρχείο δεν μπορεί να είναι τυχόν ή κενό.
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Failed Import File
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Η σεζώνη σας υπάρξει παραγωγή. Πατήστε για να ανανεώσετε το πλήρωμα και δοκιμάστε ξανά.
|
session.expired=Η σεζώνη σας υπάρξει παραγωγή. Πατήστε για να ανανεώσετε το πλήρωμα και δοκιμάστε ξανά.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Αυτόματο Μαύρισμα Κειμένου
|
|||||||
home.autoRedact.desc=Αυτόματη επεξεργασία (Μαύρισμα) κείμενου σε PDF με βάση το κείμενο εισαγωγής
|
home.autoRedact.desc=Αυτόματη επεξεργασία (Μαύρισμα) κείμενου σε PDF με βάση το κείμενο εισαγωγής
|
||||||
autoRedact.tags=ξεκράζω,φυλακίζω,εικόνο-κουπές,κρατήστε ασφαλείς,δημιουργήστε
|
autoRedact.tags=ξεκράζω,φυλακίζω,εικόνο-κουπές,κρατήστε ασφαλείς,δημιουργήστε
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF σε CSV
|
home.tableExtraxt.title=PDF σε CSV
|
||||||
home.tableExtraxt.desc=Εξάγει πίνακες από PDF μετατρέποντάς το σε CSV
|
home.tableExtraxt.desc=Εξάγει πίνακες από PDF μετατρέποντάς το σε CSV
|
||||||
tableExtraxt.tags=CSV,καταθέσεις-τάβλες,αποδιατύπωση,μετατροπή
|
tableExtraxt.tags=CSV,καταθέσεις-τάβλες,αποδιατύπωση,μετατροπή
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Αρχαίως απαιτήσεις πληροφορ
|
|||||||
login.oauth2AccessDenied=Πρόσβαση ορισμένες τοποθετήσεις
|
login.oauth2AccessDenied=Πρόσβαση ορισμένες τοποθετήσεις
|
||||||
login.oauth2InvalidTokenResponse=Απάντηση άρκετο ρόταμα
|
login.oauth2InvalidTokenResponse=Απάντηση άρκετο ρόταμα
|
||||||
login.oauth2InvalidIdToken=Λιδός όρκου μη αρκετός
|
login.oauth2InvalidIdToken=Λιδός όρκου μη αρκετός
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=Ο χρήστης είναι δευτεροβαθμιακά διακωμένος, το σύστημα άλλαξε τον καθώς βρεθεί. Παρακαλείστε τους αρχηγούς να επιβεβαιώσουν τη συμπεριφορά χρήστη.
|
login.userIsDisabled=Ο χρήστης είναι δευτεροβαθμιακά διακωμένος, το σύστημα άλλαξε τον καθώς βρεθεί. Παρακαλείστε τους αρχηγούς να επιβεβαιώσουν τη συμπεριφορά χρήστη.
|
||||||
login.alreadyLoggedIn=Είστε ήδη σύνδεδες σε
|
login.alreadyLoggedIn=Είστε ήδη σύνδεδες σε
|
||||||
login.alreadyLoggedIn2=κατοχόι. Παρακαλώ δυσκέντρωση τους και προσπαθήστε ξανά.
|
login.alreadyLoggedIn2=κατοχόι. Παρακαλώ δυσκέντρωση τους και προσπαθήστε ξανά.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Εμπνευσμένη Προσθήκη Ανάκρ
|
|||||||
autoRedact.convertPDFToImageLabel=Μετατροπή PDF σε PDF-Εικόνα (Χρησιμοποιείται για την αφαίρεση κειμένου πίσω από το πλαίσιο)
|
autoRedact.convertPDFToImageLabel=Μετατροπή PDF σε PDF-Εικόνα (Χρησιμοποιείται για την αφαίρεση κειμένου πίσω από το πλαίσιο)
|
||||||
autoRedact.submitButton=Υποβολή
|
autoRedact.submitButton=Υποβολή
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Εμφάνιση Javascript
|
showJS.title=Εμφάνιση Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Επιδιόρθωση
|
repair.title=Επιδιόρθωση
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Κλίμακα του γκρι
|
|||||||
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
|
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
|
||||||
pdfToImage.submit=Μετατροπή
|
pdfToImage.submit=Μετατροπή
|
||||||
pdfToImage.info=Δεν είναι ιστάμενος Python. Είναι απαιτήτων για τη μετατροπή σε WebP.
|
pdfToImage.info=Δεν είναι ιστάμενος Python. Είναι απαιτήτων για τη μετατροπή σε WebP.
|
||||||
|
pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Διαλύστε το PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Pages
|
|||||||
loading=Loading...
|
loading=Loading...
|
||||||
addToDoc=Add to Document
|
addToDoc=Add to Document
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Terms and Conditions
|
legal.terms=Terms and Conditions
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=File not found
|
database.fileNotFound=File not found
|
||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=File must not be null or empty
|
||||||
database.failedImportFile=Failed to import file
|
database.failedImportFile=Failed to import file
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Your session has expired. Please refresh the page and try again.
|
session.expired=Your session has expired. Please refresh the page and try again.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Redact
|
|||||||
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
|
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
|
||||||
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF to CSV
|
home.tableExtraxt.title=PDF to CSV
|
||||||
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
||||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
|||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
login.alreadyLoggedIn=You are already logged in to
|
login.alreadyLoggedIn=You are already logged in to
|
||||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Custom Extra Padding
|
|||||||
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
autoRedact.submitButton=Submit
|
autoRedact.submitButton=Submit
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Show Javascript
|
showJS.title=Show Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Repair
|
repair.title=Repair
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Greyscale
|
|||||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||||
pdfToImage.submit=Convert
|
pdfToImage.submit=Convert
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Pages
|
|||||||
loading=Loading...
|
loading=Loading...
|
||||||
addToDoc=Add to Document
|
addToDoc=Add to Document
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Terms and Conditions
|
legal.terms=Terms and Conditions
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=File not Found
|
database.fileNotFound=File not Found
|
||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=File must not be null or empty
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Failed Import File
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Your session has expired. Please refresh the page and try again.
|
session.expired=Your session has expired. Please refresh the page and try again.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Redact
|
|||||||
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
|
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
|
||||||
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF to CSV
|
home.tableExtraxt.title=PDF to CSV
|
||||||
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
||||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
|||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
login.alreadyLoggedIn=You are already logged in to
|
login.alreadyLoggedIn=You are already logged in to
|
||||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Custom Extra Padding
|
|||||||
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
autoRedact.submitButton=Submit
|
autoRedact.submitButton=Submit
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Show Javascript
|
showJS.title=Show Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Repair
|
repair.title=Repair
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Grayscale
|
|||||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||||
pdfToImage.submit=Convert
|
pdfToImage.submit=Convert
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Páginas
|
|||||||
loading=Cargando...
|
loading=Cargando...
|
||||||
addToDoc=Agregar al Documento
|
addToDoc=Agregar al Documento
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Política de Privacidad
|
legal.privacy=Política de Privacidad
|
||||||
legal.terms=Términos y Condiciones
|
legal.terms=Términos y Condiciones
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=Archivo no encontrado
|
database.fileNotFound=Archivo no encontrado
|
||||||
database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
|
database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
|
||||||
database.failedImportFile=Archivo de importación fallido
|
database.failedImportFile=Archivo de importación fallido
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Tu sesión ha caducado. Actualice la página e inténtelo de nuevo.
|
session.expired=Tu sesión ha caducado. Actualice la página e inténtelo de nuevo.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Redactar
|
|||||||
home.autoRedact.desc=Redactar automáticamente (ocultar) texto en un PDF según el texto introducido
|
home.autoRedact.desc=Redactar automáticamente (ocultar) texto en un PDF según el texto introducido
|
||||||
autoRedact.tags=Redactar,Ocultar,ocultar,negro,subrayador,oculto
|
autoRedact.tags=Redactar,Ocultar,ocultar,negro,subrayador,oculto
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF a CSV
|
home.tableExtraxt.title=PDF a CSV
|
||||||
home.tableExtraxt.desc=Extraer Tablas de un PDF convirtiéndolas a CSV
|
home.tableExtraxt.desc=Extraer Tablas de un PDF convirtiéndolas a CSV
|
||||||
tableExtraxt.tags=CSV,Extraer tabla,extraer,convertir
|
tableExtraxt.tags=CSV,Extraer tabla,extraer,convertir
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Solicitud no válida
|
|||||||
login.oauth2AccessDenied=Acceso denegado
|
login.oauth2AccessDenied=Acceso denegado
|
||||||
login.oauth2InvalidTokenResponse=Respuesta de token no válida
|
login.oauth2InvalidTokenResponse=Respuesta de token no válida
|
||||||
login.oauth2InvalidIdToken=Token de identificación no válido
|
login.oauth2InvalidIdToken=Token de identificación no válido
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=El usuario está desactivado, actualmente el acceso está bloqueado para ese nombre de usuario. Por favor, póngase en contacto con el administrador.
|
login.userIsDisabled=El usuario está desactivado, actualmente el acceso está bloqueado para ese nombre de usuario. Por favor, póngase en contacto con el administrador.
|
||||||
login.alreadyLoggedIn=Ya has iniciado sesión en
|
login.alreadyLoggedIn=Ya has iniciado sesión en
|
||||||
login.alreadyLoggedIn2=dispositivos. Cierra sesión en los dispositivos y vuelve a intentarlo.
|
login.alreadyLoggedIn2=dispositivos. Cierra sesión en los dispositivos y vuelve a intentarlo.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Extra Padding personalizado
|
|||||||
autoRedact.convertPDFToImageLabel=Convertir PDF a imagen PDF (Utilizado para quitar el texto detrás del cajetín)
|
autoRedact.convertPDFToImageLabel=Convertir PDF a imagen PDF (Utilizado para quitar el texto detrás del cajetín)
|
||||||
autoRedact.submitButton=Enviar
|
autoRedact.submitButton=Enviar
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Mostrar Javascript
|
showJS.title=Mostrar Javascript
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparar
|
repair.title=Reparar
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Escala de grises
|
|||||||
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
|
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
|
||||||
pdfToImage.submit=Convertir
|
pdfToImage.submit=Convertir
|
||||||
pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP.
|
pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP.
|
||||||
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Dividir PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ pages=Pages
|
|||||||
loading=Loading...
|
loading=Loading...
|
||||||
addToDoc=Add to Document
|
addToDoc=Add to Document
|
||||||
reset=Reset
|
reset=Reset
|
||||||
|
apply=Apply
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Terms and Conditions
|
legal.terms=Terms and Conditions
|
||||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
|||||||
database.fileNotFound=File not Found
|
database.fileNotFound=File not Found
|
||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=File must not be null or empty
|
||||||
database.failedImportFile=Failed Import File
|
database.failedImportFile=Failed Import File
|
||||||
|
database.notSupported=This function is not available for your database connection.
|
||||||
|
|
||||||
session.expired=Your session has expired. Please refresh the page and try again.
|
session.expired=Your session has expired. Please refresh the page and try again.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Refresh Page
|
||||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Idatzi
|
|||||||
home.autoRedact.desc=Auto Idatzi testua pdf fitxategian sarrerako testuan oinarritua
|
home.autoRedact.desc=Auto Idatzi testua pdf fitxategian sarrerako testuan oinarritua
|
||||||
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
|
||||||
|
|
||||||
|
home.redact.title=Manual Redaction
|
||||||
|
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
|
||||||
|
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
|
||||||
|
|
||||||
home.tableExtraxt.title=PDF to CSV
|
home.tableExtraxt.title=PDF to CSV
|
||||||
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV
|
||||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
|||||||
login.oauth2AccessDenied=Access Denied
|
login.oauth2AccessDenied=Access Denied
|
||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
|
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||||
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
login.alreadyLoggedIn=You are already logged in to
|
login.alreadyLoggedIn=You are already logged in to
|
||||||
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Custom Extra Padding
|
|||||||
autoRedact.convertPDFToImageLabel=Bihurtu PDF fitxategi bat PDF-Irudi-ra (kaxaren atzean testua ezabatzeko erabilia)
|
autoRedact.convertPDFToImageLabel=Bihurtu PDF fitxategi bat PDF-Irudi-ra (kaxaren atzean testua ezabatzeko erabilia)
|
||||||
autoRedact.submitButton=Bidali
|
autoRedact.submitButton=Bidali
|
||||||
|
|
||||||
|
#redact
|
||||||
|
redact.title=Manual Redaction
|
||||||
|
redact.header=Manual Redaction
|
||||||
|
redact.submit=Redact
|
||||||
|
redact.textBasedRedaction=Text based Redaction
|
||||||
|
redact.pageBasedRedaction=Page-based Redaction
|
||||||
|
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
|
||||||
|
redact.pageRedactionNumbers.title=Pages
|
||||||
|
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
redact.redactionColor.title=Redaction Color
|
||||||
|
redact.export=Export
|
||||||
|
redact.upload=Upload
|
||||||
|
redact.boxRedaction=Box draw redaction
|
||||||
|
redact.zoom=Zoom
|
||||||
|
redact.zoomIn=Zoom in
|
||||||
|
redact.zoomOut=Zoom out
|
||||||
|
redact.nextPage=Next Page
|
||||||
|
redact.previousPage=Previous Page
|
||||||
|
redact.toggleSidebar=Toggle Sidebar
|
||||||
|
redact.showThumbnails=Show Thumbnails
|
||||||
|
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
|
||||||
|
redact.showAttatchments=Show Attachments
|
||||||
|
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
|
||||||
|
redact.colourPicker=Colour Picker
|
||||||
|
redact.findCurrentOutlineItem=Find current outline item
|
||||||
|
|
||||||
#showJS
|
#showJS
|
||||||
showJS.title=Javascript erakutsi
|
showJS.title=Javascript erakutsi
|
||||||
@@ -830,6 +862,8 @@ sign.first=First page
|
|||||||
sign.last=Last page
|
sign.last=Last page
|
||||||
sign.next=Next page
|
sign.next=Next page
|
||||||
sign.previous=Previous page
|
sign.previous=Previous page
|
||||||
|
sign.maintainRatio=Toggle maintain aspect ratio
|
||||||
|
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Konpondu
|
repair.title=Konpondu
|
||||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Gris-eskala
|
|||||||
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
|
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
|
||||||
pdfToImage.submit=Bihurtu
|
pdfToImage.submit=Bihurtu
|
||||||
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
pdfToImage.info=Python is not installed. Required for WebP conversion.
|
||||||
|
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||||
|
|
||||||
|
|
||||||
#addPassword
|
#addPassword
|
||||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
|||||||
fileChooser.click=Click
|
fileChooser.click=Click
|
||||||
fileChooser.or=or
|
fileChooser.or=or
|
||||||
fileChooser.dragAndDrop=Drag & Drop
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||||
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user