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/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/UI/**/*'
|
||||
|
||||
Java:
|
||||
- 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/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/BackupNotFoundException.java'
|
||||
- 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/scorecards.yml'
|
||||
@@ -49,12 +51,17 @@ Documentation:
|
||||
|
||||
Docker:
|
||||
- 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: '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-without-ocr.sh'
|
||||
- any-glob-to-any-file: 'scripts/installFonts.sh'
|
||||
- any-glob-to-any-file: 'test.sh'
|
||||
- any-glob-to-any-file: 'test2.sh'
|
||||
|
||||
Test:
|
||||
- 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:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: read
|
||||
issues: read
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
@@ -20,7 +26,8 @@ jobs:
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'LaserKaspar' ||
|
||||
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:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
@@ -68,6 +75,9 @@ jobs:
|
||||
deploy-pr:
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -98,7 +108,9 @@ jobs:
|
||||
|
||||
- name: Get version number
|
||||
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
|
||||
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:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
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:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -6,13 +6,15 @@ on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
@@ -44,7 +46,18 @@ jobs:
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
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:
|
||||
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
|
||||
# (github.event_name == 'pull_request' &&
|
||||
@@ -82,7 +95,7 @@ jobs:
|
||||
|
||||
- name: Install Docker Compose
|
||||
run: |
|
||||
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo 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
|
||||
|
||||
- name: Set up Python
|
||||
@@ -92,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Pip requirements
|
||||
run: |
|
||||
pip install -r ./cucumber/requirements.txt
|
||||
pip install --require-hashes -r ./cucumber/requirements.txt
|
||||
|
||||
- name: Run Docker Compose Tests
|
||||
run: |
|
||||
|
||||
173
.github/workflows/check_properties.yml
vendored
173
.github/workflows/check_properties.yml
vendored
@@ -8,12 +8,14 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read # Allow read access to repository content
|
||||
issues: write # Allow posting comments on issues/PRs
|
||||
|
||||
jobs:
|
||||
check-files:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write # Allow posting comments on issues/PRs
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||
@@ -26,7 +28,28 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
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
|
||||
id: fetch-pr-changes
|
||||
@@ -34,55 +57,114 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
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..."
|
||||
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
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Determining reference file..."
|
||||
REPO_OWNER=$(gh pr view ${{ github.event.pull_request.number }} --json author -q '.author.login') # Get PR author's username
|
||||
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
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
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
|
||||
while IFS= read -r file; do
|
||||
mkdir -p "pr-branch/$(dirname "$file")" # Create directories for files
|
||||
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
|
||||
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
|
||||
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
|
||||
const branch = "${{ steps.get-pr-data.outputs.branch }}";
|
||||
|
||||
# Generate a list of files without the "pr-branch/" prefix
|
||||
find pr-branch/ -type f | awk -F'pr-branch/' '{print $2}' > file_list.txt
|
||||
console.log(`Determining reference file for PR #${prNumber}`);
|
||||
|
||||
mapfile -t FILES_LIST < file_list.txt # Read the file list into an array
|
||||
FILES_LIST_STR="${FILES_LIST[*]}" # Join array into a space-separated string
|
||||
echo "FILES_LIST=${FILES_LIST_STR}" >> $GITHUB_ENV # Export the file list to the environment
|
||||
echo "Changed files: ${FILES_LIST_STR}"
|
||||
// Validate inputs
|
||||
const validateInput = (input, regex, name) => {
|
||||
if (!regex.test(input)) {
|
||||
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
|
||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
||||
echo "Using PR branch reference file"
|
||||
REFERENCE_FILE="pr-branch-messages_en_GB.properties"
|
||||
gh api repos/$REPO_OWNER/$REPO_NAME/contents/src/main/resources/messages_en_GB.properties?ref=${{ github.event.pull_request.head.ref }} \
|
||||
--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
|
||||
// Get the list of changed files in the PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
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
|
||||
id: run-check
|
||||
@@ -92,7 +174,8 @@ jobs:
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--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
|
||||
id: capture-output
|
||||
@@ -124,13 +207,13 @@ jobs:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
const issueNumber = context.issue.number;
|
||||
|
||||
// Find existing comment
|
||||
const comments = await github.rest.issues.listComments({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
||||
@@ -152,7 +235,7 @@ jobs:
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
issue_number: issueNumber,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
});
|
||||
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:
|
||||
- "build.gradle"
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate-license-report:
|
||||
@@ -52,7 +53,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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:
|
||||
- cron: "30 20 * * *"
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
|
||||
5
.github/workflows/multiOSReleases.yml
vendored
5
.github/workflows/multiOSReleases.yml
vendored
@@ -5,7 +5,8 @@ on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-installers:
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
|
||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
with:
|
||||
gradle-version: 8.7
|
||||
gradle-version: 8.12
|
||||
|
||||
# Install Windows dependencies
|
||||
- name: Install WiX Toolset
|
||||
|
||||
7
.github/workflows/pre_commit.yml
vendored
7
.github/workflows/pre_commit.yml
vendored
@@ -4,7 +4,8 @@ on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update:
|
||||
@@ -19,7 +20,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: Run Pre-Commit Hooks
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
git diff --staged --quiet || git commit -m ":file_folder: pre-commit
|
||||
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
|
||||
- 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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
|
||||
@@ -33,7 +32,7 @@ jobs:
|
||||
|
||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
with:
|
||||
gradle-version: 8.7
|
||||
gradle-version: 8.12
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: ./gradlew clean build
|
||||
@@ -42,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@v3.7.0
|
||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||
with:
|
||||
cosign-release: 'v2.4.1'
|
||||
|
||||
|
||||
5
.github/workflows/releaseArtifacts.yml
vendored
5
.github/workflows/releaseArtifacts.yml
vendored
@@ -5,7 +5,8 @@ on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
push:
|
||||
@@ -37,7 +38,7 @@ jobs:
|
||||
|
||||
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
with:
|
||||
gradle-version: 8.7
|
||||
gradle-version: 8.12
|
||||
|
||||
- name: Generate jar (With Security=${{ matrix.enable_security }})
|
||||
run: ./gradlew clean createExe
|
||||
|
||||
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -5,7 +5,8 @@ on:
|
||||
- cron: "30 0 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
3
.github/workflows/swagger.yml
vendored
3
.github/workflows/swagger.yml
vendored
@@ -6,7 +6,8 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
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"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-readme:
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
python-version: "3.12"
|
||||
- name: Install dependencies
|
||||
run: pip install tomlkit
|
||||
- name: Sync README
|
||||
@@ -42,7 +43,7 @@ jobs:
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README
|
||||
> Made via sync_files.yml" || echo "no changes"
|
||||
- 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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: Update files
|
||||
|
||||
7
.github/workflows/update-translations.yml
vendored
7
.github/workflows/update-translations.yml
vendored
@@ -6,7 +6,8 @@ on:
|
||||
paths:
|
||||
- "src/main/resources/messages_en_GB.properties"
|
||||
|
||||
permissions: read-all
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-translations-main:
|
||||
@@ -27,7 +28,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
@@ -50,7 +51,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
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:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update translation files"
|
||||
|
||||
22
.gitignore
vendored
22
.gitignore
vendored
@@ -146,19 +146,37 @@ out/
|
||||
# cucumber
|
||||
/cucumber/reports/**
|
||||
|
||||
# Certs
|
||||
# Certs and Security Files
|
||||
*.p12
|
||||
*.pk8
|
||||
*.pem
|
||||
*.crt
|
||||
*.cer
|
||||
*.cert
|
||||
*.der
|
||||
*.key
|
||||
*.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
|
||||
.ruff_cache
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
**/jcef-bundle/
|
||||
**/jcef-bundle/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the application
|
||||
FROM gradle:8.11-jdk17 AS build
|
||||
FROM gradle:8.12-jdk17 AS build
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
78
README.md
78
README.md
@@ -4,9 +4,9 @@
|
||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||
[](https://discord.gg/HYmhKj45pU)
|
||||
[](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)
|
||||
|
||||
|
||||
<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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
@@ -169,4 +171,4 @@ Join our community:
|
||||
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md)
|
||||
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||
- [Developer Guide](DeveloperGuide.md)
|
||||
- [Developer Guide](DeveloperGuide.md)
|
||||
|
||||
44
build.gradle
44
build.gradle
@@ -27,7 +27,7 @@ ext {
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.36.5"
|
||||
version = "0.36.6"
|
||||
|
||||
|
||||
java {
|
||||
@@ -52,13 +52,15 @@ sourceSets {
|
||||
java {
|
||||
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
|
||||
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/UserController.java"
|
||||
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java"
|
||||
exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
|
||||
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
|
||||
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
|
||||
exclude "stirling/software/SPDF/model/AttemptCounter.java"
|
||||
exclude "stirling/software/SPDF/model/Authority.java"
|
||||
exclude "stirling/software/SPDF/model/BackupNotFoundException.java"
|
||||
exclude "stirling/software/SPDF/model/PersistentLogin.java"
|
||||
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
||||
exclude "stirling/software/SPDF/model/User.java"
|
||||
@@ -69,7 +71,29 @@ sourceSets {
|
||||
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 {
|
||||
launcherAsService = false
|
||||
appVersion = project.version
|
||||
winConsole = false
|
||||
winDirChooser = true
|
||||
winMenu = true
|
||||
winShortcut = true
|
||||
winPerUserInstall = true
|
||||
|
||||
winConsole = false
|
||||
winMenu = true // Creates start menu entry
|
||||
winShortcut = true // Creates desktop shortcut
|
||||
winShortcutPrompt = true // Lets user choose whether to create shortcuts
|
||||
winDirChooser = true // Allows users to choose installation directory
|
||||
winPerUserInstall = false
|
||||
winMenuGroup = "Stirling Software"
|
||||
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
|
||||
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
|
||||
@@ -257,7 +283,7 @@ spotless {
|
||||
// rules=['unused-dependency']
|
||||
// }
|
||||
tasks.wrapper {
|
||||
gradleVersion = "8.7"
|
||||
gradleVersion = "8.12"
|
||||
}
|
||||
//tasks.withType(JavaCompile) {
|
||||
// options.compilerArgs << "-Xlint:deprecation"
|
||||
@@ -297,10 +323,12 @@ dependencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$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'
|
||||
// Don't upgrade h2database
|
||||
runtimeOnly "com.h2database:h2:2.3.232"
|
||||
runtimeOnly "org.postgresql:postgresql:42.7.4"
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@general
|
||||
Feature: API Validation
|
||||
|
||||
|
||||
|
||||
@split-pdf-by-sections @positive
|
||||
Scenario Outline: split-pdf-by-sections with different parameters
|
||||
Given I generate a PDF file as "fileInput"
|
||||
@@ -66,7 +66,7 @@ Feature: API Validation
|
||||
| pageNumbers | file_count |
|
||||
| 1,3,5-9 | 8 |
|
||||
| all | 20 |
|
||||
| 2n+1 | 11 |
|
||||
| 2n+1 | 10 |
|
||||
| 3n | 7 |
|
||||
|
||||
|
||||
@@ -106,9 +106,9 @@ Feature: API Validation
|
||||
And the response ZIP should contain 2 files
|
||||
And the response file should have size greater than 0
|
||||
And the response status code should be 200
|
||||
|
||||
|
||||
Examples:
|
||||
| format |
|
||||
| png |
|
||||
| format |
|
||||
| png |
|
||||
| gif |
|
||||
| jpeg |
|
||||
| jpeg |
|
||||
|
||||
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
|
||||
PyPDF2
|
||||
reportlab
|
||||
PyCryptodome
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.10
|
||||
# by the following command:
|
||||
#
|
||||
# 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:
|
||||
- 8080:8080
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
- ./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"
|
||||
|
||||
@@ -14,9 +14,9 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /stirling/latest/data:/usr/share/tessdata:rw
|
||||
- /stirling/latest/config:/configs:rw
|
||||
- /stirling/latest/logs:/logs:rw
|
||||
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "true"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
|
||||
@@ -247,6 +247,11 @@ ignore = [
|
||||
'showJS.tags',
|
||||
]
|
||||
|
||||
[zh_BO]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[zh_CN]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
|
||||
@@ -27,4 +27,9 @@ public class EEAppConfig {
|
||||
public boolean runningEnterpriseEdition() {
|
||||
return licenseKeyChecker.getEnterpriseEnabledResult();
|
||||
}
|
||||
|
||||
@Bean(name = "SSOAutoLogin")
|
||||
public boolean ssoAutoLogin() {
|
||||
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ public class KeygenLicenseVerifier {
|
||||
.build();
|
||||
|
||||
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());
|
||||
if (response.statusCode() == 200) {
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class LicenseKeyChecker {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private boolean enterpriseEnbaledResult = false;
|
||||
private boolean enterpriseEnabledResult = false;
|
||||
|
||||
@Autowired
|
||||
public LicenseKeyChecker(
|
||||
@@ -35,12 +35,12 @@ public class LicenseKeyChecker {
|
||||
|
||||
private void checkLicense() {
|
||||
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||
enterpriseEnbaledResult = false;
|
||||
enterpriseEnabledResult = false;
|
||||
} else {
|
||||
enterpriseEnbaledResult =
|
||||
enterpriseEnabledResult =
|
||||
licenseService.verifyLicense(
|
||||
applicationProperties.getEnterpriseEdition().getKey());
|
||||
if (enterpriseEnbaledResult) {
|
||||
if (enterpriseEnabledResult) {
|
||||
log.info("License key is valid.");
|
||||
} else {
|
||||
log.info("License key is invalid.");
|
||||
@@ -55,6 +55,6 @@ public class LicenseKeyChecker {
|
||||
}
|
||||
|
||||
public boolean getEnterpriseEnabledResult() {
|
||||
return enterpriseEnbaledResult;
|
||||
return enterpriseEnabledResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -24,15 +25,17 @@ import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.SPDF.config.ConfigInitializer;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@Slf4j
|
||||
public class SPdfApplication {
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
public class SPDFApplication {
|
||||
|
||||
private static String baseUrlStatic;
|
||||
private static String serverPortStatic;
|
||||
private static String baseUrlStatic;
|
||||
|
||||
private final Environment env;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final WebBrowser webBrowser;
|
||||
@@ -40,7 +43,7 @@ public class SPdfApplication {
|
||||
@Value("${baseUrl:http://localhost}")
|
||||
private String baseUrl;
|
||||
|
||||
public SPdfApplication(
|
||||
public SPDFApplication(
|
||||
Environment env,
|
||||
ApplicationProperties applicationProperties,
|
||||
@Autowired(required = false) WebBrowser webBrowser) {
|
||||
@@ -49,42 +52,41 @@ public class SPdfApplication {
|
||||
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 {
|
||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
||||
SpringApplication app = new SpringApplication(SPDFApplication.class);
|
||||
|
||||
Properties props = new Properties();
|
||||
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
System.setProperty("java.awt.headless", "false");
|
||||
app.setHeadless(false);
|
||||
props.put("java.awt.headless", "false");
|
||||
props.put("spring.main.web-application-type", "servlet");
|
||||
}
|
||||
app.setAdditionalProfiles("default");
|
||||
app.addInitializers(new ConfigInitializer());
|
||||
Map<String, String> propertyFiles = new HashMap<>();
|
||||
// External config files
|
||||
if (Files.exists(Paths.get("configs/settings.yml"))) {
|
||||
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
|
||||
} else {
|
||||
log.warn("External configuration file 'configs/settings.yml' does not exist.");
|
||||
|
||||
app.setAdditionalProfiles(getActiveProfile(args));
|
||||
|
||||
ConfigInitializer initializer = new ConfigInitializer();
|
||||
try {
|
||||
initializer.ensureConfigExists();
|
||||
} catch (IOException | URISyntaxException e) {
|
||||
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 =
|
||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||
if (!existingLocation.isEmpty()) {
|
||||
@@ -92,57 +94,39 @@ public class SPdfApplication {
|
||||
}
|
||||
propertyFiles.put(
|
||||
"spring.config.additional-location",
|
||||
existingLocation + "file:configs/custom_settings.yml");
|
||||
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
|
||||
} 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();
|
||||
|
||||
if (!propertyFiles.isEmpty()) {
|
||||
finalProps.putAll(
|
||||
Collections.singletonMap(
|
||||
"spring.config.additional-location",
|
||||
propertyFiles.get("spring.config.additional-location")));
|
||||
}
|
||||
|
||||
if (!props.isEmpty()) {
|
||||
finalProps.putAll(props);
|
||||
}
|
||||
app.setDefaultProperties(finalProps);
|
||||
|
||||
app.run(args);
|
||||
|
||||
// Ensure directories are created
|
||||
try {
|
||||
Files.createDirectories(Path.of("customFiles/static/"));
|
||||
Files.createDirectories(Path.of("customFiles/templates/"));
|
||||
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
||||
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating directories: {}", e.getMessage());
|
||||
}
|
||||
|
||||
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
|
||||
public void init() {
|
||||
baseUrlStatic = this.baseUrl;
|
||||
@@ -173,6 +157,17 @@ public class SPdfApplication {
|
||||
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
|
||||
public void cleanup() {
|
||||
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() {
|
||||
return baseUrlStatic;
|
||||
}
|
||||
|
||||
public static String getStaticPort() {
|
||||
return serverPortStatic;
|
||||
}
|
||||
|
||||
public String getNonStaticPort() {
|
||||
return serverPortStatic;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@@ -72,7 +73,8 @@ public class DesktopBrowser implements WebBrowser {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
|
||||
builder.setInstallDir(
|
||||
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
@@ -99,8 +101,16 @@ public class DesktopBrowser implements WebBrowser {
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
settings.cache_path = new File("jcef-bundle").getAbsolutePath();
|
||||
settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
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.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
@@ -212,6 +222,9 @@ public class DesktopBrowser implements WebBrowser {
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
final long initStartTime = System.currentTimeMillis();
|
||||
log.info("Setting up load handler at: {}", initStartTime);
|
||||
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
@@ -220,32 +233,77 @@ public class DesktopBrowser implements WebBrowser {
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
log.debug(
|
||||
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
|
||||
+ "browserInitialized: {}, Time elapsed: {}ms",
|
||||
isLoading,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
browserInitialized,
|
||||
System.currentTimeMillis() - initStartTime);
|
||||
|
||||
if (!isLoading && !browserInitialized) {
|
||||
log.info(
|
||||
"Browser finished loading, preparing to initialize UI components");
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
Timer timer =
|
||||
new Timer(
|
||||
500,
|
||||
e -> {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Starting UI initialization sequence");
|
||||
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(1280, 800);
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
});
|
||||
timer.setRepeats(false);
|
||||
timer.start();
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(1280, 800);
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
log.info(
|
||||
"Browser component focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing browser",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
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 JPanel mainPanel;
|
||||
private final JLabel brandLabel;
|
||||
private long startTime;
|
||||
|
||||
public LoadingWindow(Frame parent, String initialUrl) {
|
||||
super(parent, "Initializing Stirling-PDF", true);
|
||||
startTime = System.currentTimeMillis();
|
||||
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
|
||||
|
||||
// Initialize components
|
||||
mainPanel = new JPanel();
|
||||
@@ -29,8 +32,8 @@ public class LoadingWindow extends JDialog {
|
||||
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.weightx = 1.0; // Add horizontal weight
|
||||
gbc.weighty = 0.0; // Add vertical weight
|
||||
gbc.weightx = 1.0;
|
||||
gbc.weighty = 0.0;
|
||||
|
||||
// Add icon
|
||||
try {
|
||||
@@ -43,12 +46,14 @@ public class LoadingWindow extends JDialog {
|
||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
gbc.gridy = 0;
|
||||
mainPanel.add(iconLabel, gbc);
|
||||
log.debug("Icon loaded and scaled successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load icon", e);
|
||||
}
|
||||
|
||||
// URL Label with explicit size
|
||||
brandLabel = new JLabel(initialUrl);
|
||||
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
@@ -63,6 +68,7 @@ public class LoadingWindow extends JDialog {
|
||||
statusLabel.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 2;
|
||||
mainPanel.add(statusLabel, gbc);
|
||||
|
||||
// Progress bar with explicit size
|
||||
progressBar = new JProgressBar(0, 100);
|
||||
progressBar.setStringPainted(true);
|
||||
@@ -82,33 +88,78 @@ public class LoadingWindow extends JDialog {
|
||||
setAlwaysOnTop(true);
|
||||
setProgress(0);
|
||||
setStatus("Starting...");
|
||||
|
||||
log.info(
|
||||
"LoadingWindow initialization completed in {}ms",
|
||||
System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
progressBar.setValue(Math.min(Math.max(progress, 0), 100));
|
||||
progressBar.setString(progress + "%");
|
||||
int validProgress = Math.min(Math.max(progress, 0), 100);
|
||||
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.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating progress", e);
|
||||
log.error("Error updating progress to " + progress, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatus(final String status) {
|
||||
log.info(status);
|
||||
log.info(
|
||||
"Status update at {}ms - Setting status to: {}",
|
||||
System.currentTimeMillis() - startTime,
|
||||
status);
|
||||
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
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.repaint();
|
||||
} 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;
|
||||
}
|
||||
|
||||
@Bean(name = "watchedFoldersDir")
|
||||
public String watchedFoldersDir() {
|
||||
return "./pipeline/watchedFolders/";
|
||||
}
|
||||
|
||||
@Bean(name = "finishedFoldersDir")
|
||||
public String finishedFoldersDir() {
|
||||
return "./pipeline/finishedFolders/";
|
||||
}
|
||||
|
||||
@Bean(name = "directoryFilter")
|
||||
public Predicate<Path> processOnlyFiles() {
|
||||
return path -> {
|
||||
|
||||
@@ -16,27 +16,15 @@ import org.simpleyaml.configuration.comments.CommentType;
|
||||
import org.simpleyaml.configuration.file.YamlFile;
|
||||
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
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 class ConfigInitializer {
|
||||
|
||||
public void ensureConfigExists() throws IOException, URISyntaxException {
|
||||
// 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
|
||||
if (Files.notExists(destPath)) {
|
||||
@@ -53,10 +41,11 @@ public class ConfigInitializer
|
||||
"Resource file not found: settings.yml.template");
|
||||
}
|
||||
}
|
||||
log.info("Created settings file from template");
|
||||
} else {
|
||||
|
||||
// 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
|
||||
URL settingsTemplateResource =
|
||||
getClass().getClassLoader().getResource("settings.yml.template");
|
||||
@@ -120,7 +109,7 @@ public class ConfigInitializer
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
Files.createFile(customSettingsPath);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -135,6 +136,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Security", "remove-cert-sign");
|
||||
addEndpointToGroup("Security", "sanitize-pdf");
|
||||
addEndpointToGroup("Security", "auto-redact");
|
||||
addEndpointToGroup("Security", "redact");
|
||||
|
||||
// Adding endpoints to "Other" group
|
||||
addEndpointToGroup("Other", "ocr-pdf");
|
||||
@@ -234,6 +236,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "markdown-to-pdf");
|
||||
addEndpointToGroup("Java", "show-javascript");
|
||||
addEndpointToGroup("Java", "auto-redact");
|
||||
addEndpointToGroup("Java", "redact");
|
||||
addEndpointToGroup("Java", "pdf-to-csv");
|
||||
addEndpointToGroup("Java", "split-by-size-or-count");
|
||||
addEndpointToGroup("Java", "overlay-pdf");
|
||||
@@ -265,20 +268,26 @@ public class EndpointConfiguration {
|
||||
}
|
||||
|
||||
private void processEnvironmentConfigs() {
|
||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||
if (!bookAndHtmlFormatsInstalled) {
|
||||
groupsToRemove.add("Calibre");
|
||||
}
|
||||
if (endpointsToRemove != null) {
|
||||
for (String endpoint : endpointsToRemove) {
|
||||
disableEndpoint(endpoint.trim());
|
||||
}
|
||||
}
|
||||
if (applicationProperties != null && applicationProperties.getEndpoints() != null) {
|
||||
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
|
||||
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
|
||||
|
||||
if (groupsToRemove != null) {
|
||||
for (String group : groupsToRemove) {
|
||||
disableGroup(group.trim());
|
||||
if (!bookAndHtmlFormatsInstalled) {
|
||||
if (groupsToRemove == null) {
|
||||
groupsToRemove = new ArrayList<>();
|
||||
}
|
||||
groupsToRemove.add("Calibre");
|
||||
}
|
||||
if (endpointsToRemove != null) {
|
||||
for (String endpoint : endpointsToRemove) {
|
||||
disableEndpoint(endpoint.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (groupsToRemove != null) {
|
||||
for (String group : groupsToRemove) {
|
||||
disableGroup(group.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
||||
String characterEncoding,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
Resource resource =
|
||||
resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
|
||||
resourceLoader.getResource(
|
||||
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
|
||||
try {
|
||||
if (resource.exists() && resource.isReadable()) {
|
||||
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) {
|
||||
// Handler for external static resources
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("file:customFiles/static/", "classpath:/static/");
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||
throws IOException {
|
||||
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||
factory.setResources(encodedResource.getResource());
|
||||
|
||||
Properties properties = factory.getObject();
|
||||
|
||||
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 lombok.AllArgsConstructor;
|
||||
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.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
// Construct URLs required for SAML configuration
|
||||
String serverUrl =
|
||||
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
|
||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
@@ -1,49 +1,56 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
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.Role;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@Component
|
||||
public class InitialSecuritySetup {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private final DatabaseBackupInterface databaseBackupHelper;
|
||||
private final DatabaseInterface databaseService;
|
||||
|
||||
public InitialSecuritySetup(
|
||||
UserService userService,
|
||||
ApplicationProperties applicationProperties,
|
||||
DatabaseBackupInterface databaseBackupHelper) {
|
||||
DatabaseInterface databaseService) {
|
||||
this.userService = userService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.databaseBackupHelper = databaseBackupHelper;
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws IllegalArgumentException, IOException {
|
||||
if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
|
||||
databaseBackupHelper.importDatabase();
|
||||
} else if (!userService.hasUsers()) {
|
||||
initializeAdminUser();
|
||||
} else {
|
||||
databaseBackupHelper.exportDatabase();
|
||||
public void init() {
|
||||
try {
|
||||
if (databaseService.hasBackup()) {
|
||||
databaseService.importDatabase();
|
||||
}
|
||||
|
||||
if (!userService.hasUsers()) {
|
||||
initializeAdminUser();
|
||||
}
|
||||
|
||||
userService.migrateOauth2ToSSO();
|
||||
initializeInternalApiUser();
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
log.error("Failed to initialize security setup.", e);
|
||||
System.exit(1);
|
||||
}
|
||||
initializeInternalApiUser();
|
||||
}
|
||||
|
||||
private void initializeAdminUser() throws IOException {
|
||||
private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
|
||||
String initialUsername =
|
||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||
String initialPassword =
|
||||
@@ -52,36 +59,34 @@ public class InitialSecuritySetup {
|
||||
&& !initialUsername.isEmpty()
|
||||
&& initialPassword != null
|
||||
&& !initialPassword.isEmpty()
|
||||
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
|
||||
try {
|
||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||
log.info("Admin user created: " + initialUsername);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Failed to initialize security setup", e);
|
||||
System.exit(1);
|
||||
}
|
||||
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) {
|
||||
|
||||
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
|
||||
log.info("Admin user created: {}", initialUsername);
|
||||
} else {
|
||||
createDefaultAdminUser();
|
||||
}
|
||||
}
|
||||
|
||||
private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
|
||||
private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException {
|
||||
String defaultUsername = "admin";
|
||||
String defaultPassword = "stirling";
|
||||
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
|
||||
|
||||
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
|
||||
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())) {
|
||||
userService.saveUser(
|
||||
Role.INTERNAL_API_USER.getRoleId(),
|
||||
UUID.randomUUID().toString(),
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
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.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
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.configuration.EnableWebSecurity;
|
||||
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.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
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.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 org.springframework.security.web.SecurityFilterChain;
|
||||
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.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.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.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.PersistentLoginRepository;
|
||||
|
||||
@@ -72,7 +49,7 @@ import stirling.software.SPDF.repository.PersistentLoginRepository;
|
||||
public class SecurityConfiguration {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
@Lazy private final UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
@Qualifier("loginEnabled")
|
||||
private final boolean loginEnabledValue;
|
||||
@@ -86,16 +63,10 @@ public class SecurityConfiguration {
|
||||
private final FirstLoginFilter firstLoginFilter;
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
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(
|
||||
PersistentLoginRepository persistentLoginRepository,
|
||||
CustomUserDetailsService userDetailsService,
|
||||
@@ -106,7 +77,12 @@ public class SecurityConfiguration {
|
||||
UserAuthenticationFilter userAuthenticationFilter,
|
||||
LoginAttemptService loginAttemptService,
|
||||
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.userService = userService;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
@@ -117,6 +93,9 @@ public class SecurityConfiguration {
|
||||
this.firstLoginFilter = firstLoginFilter;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.persistentLoginRepository = persistentLoginRepository;
|
||||
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
|
||||
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
|
||||
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -274,7 +253,7 @@ public class SecurityConfiguration {
|
||||
userService,
|
||||
loginAttemptService))
|
||||
.userAuthoritiesMapper(
|
||||
userAuthoritiesMapper()))
|
||||
oAuth2userAuthoritiesMapper))
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
@@ -291,7 +270,7 @@ public class SecurityConfiguration {
|
||||
try {
|
||||
saml2.loginPage("/saml2")
|
||||
.relyingPartyRegistrationRepository(
|
||||
relyingPartyRegistrations())
|
||||
saml2RelyingPartyRegistrations)
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
.successHandler(
|
||||
@@ -302,8 +281,7 @@ public class SecurityConfiguration {
|
||||
.failureHandler(
|
||||
new CustomSaml2AuthenticationFailureHandler())
|
||||
.authenticationRequestResolver(
|
||||
authenticationRequestResolver(
|
||||
relyingPartyRegistrations()));
|
||||
saml2AuthenticationRequestResolver);
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
@@ -311,244 +289,11 @@ public class SecurityConfiguration {
|
||||
});
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
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() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
@@ -556,46 +301,6 @@ public class SecurityConfiguration {
|
||||
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
|
||||
public IPRateLimitingFilter rateLimitingFilter() {
|
||||
// Example limit TODO add config level
|
||||
|
||||
@@ -22,22 +22,31 @@ import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.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;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final UserService userService;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final boolean loginEnabledValue;
|
||||
|
||||
public UserAuthenticationFilter(
|
||||
@Lazy ApplicationProperties applicationProperties,
|
||||
@Lazy UserService userService,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.userService = userService;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
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
|
||||
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();
|
||||
String username = null;
|
||||
if (principal instanceof UserDetails) {
|
||||
username = ((UserDetails) principal).getUsername();
|
||||
loginMethod = LoginMethod.USERDETAILS;
|
||||
} else if (principal instanceof OAuth2User) {
|
||||
username = ((OAuth2User) principal).getName();
|
||||
loginMethod = LoginMethod.OAUTH2USER;
|
||||
OAUTH2 oAuth = securityProp.getOauth2();
|
||||
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||
loginMethod = LoginMethod.SAML2USER;
|
||||
SAML2 saml2 = securityProp.getSaml2();
|
||||
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||
} else if (principal instanceof String) {
|
||||
username = (String) principal;
|
||||
loginMethod = LoginMethod.STRINGUSER;
|
||||
}
|
||||
|
||||
// Retrieve all active sessions for the user
|
||||
List<SessionInformation> sessionsInformations =
|
||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||
|
||||
// Check if the user exists, is disabled, or needs session invalidation
|
||||
if (username != null) {
|
||||
log.debug("Validating user: {}", username);
|
||||
boolean isUserExists = userService.usernameExistsIgnoreCase(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) {
|
||||
log.info(
|
||||
"Invalidating session for disabled or non-existent user: {}", username);
|
||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
||||
sessionsInformation.expireNow();
|
||||
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUserExists) {
|
||||
// Redirect to logout if credentials are invalid
|
||||
if (!isUserExists && notSsoLogin) {
|
||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||
return;
|
||||
}
|
||||
@@ -161,6 +204,25 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
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
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -20,11 +21,12 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
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.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||
import stirling.software.SPDF.model.*;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
|
||||
@@ -42,7 +44,7 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
private final SessionPersistentRegistry sessionRegistry;
|
||||
|
||||
private final DatabaseBackupInterface databaseBackupHelper;
|
||||
private final DatabaseInterface databaseService;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@@ -52,14 +54,14 @@ public class UserService implements UserServiceInterface {
|
||||
PasswordEncoder passwordEncoder,
|
||||
MessageSource messageSource,
|
||||
SessionPersistentRegistry sessionRegistry,
|
||||
DatabaseBackupInterface databaseBackupHelper,
|
||||
DatabaseInterface databaseService,
|
||||
ApplicationProperties applicationProperties) {
|
||||
this.userRepository = userRepository;
|
||||
this.authorityRepository = authorityRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.messageSource = messageSource;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.databaseBackupHelper = databaseBackupHelper;
|
||||
this.databaseService = databaseService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@@ -76,7 +78,7 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
// Handle OAUTH2 login and user auto creation.
|
||||
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
return false;
|
||||
}
|
||||
@@ -163,12 +165,12 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
public void saveUser(String username, AuthenticationType authenticationType)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||
}
|
||||
|
||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||
}
|
||||
@@ -179,11 +181,11 @@ public class UserService implements UserServiceInterface {
|
||||
user.addAuthority(new Authority(role, user));
|
||||
user.setAuthenticationType(authenticationType);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
|
||||
public void saveUser(String username, String password)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||
}
|
||||
@@ -193,11 +195,11 @@ public class UserService implements UserServiceInterface {
|
||||
user.setEnabled(true);
|
||||
user.setAuthenticationType(AuthenticationType.WEB);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
|
||||
public void saveUser(String username, String password, String role, boolean firstLogin)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||
}
|
||||
@@ -209,11 +211,11 @@ public class UserService implements UserServiceInterface {
|
||||
user.setAuthenticationType(AuthenticationType.WEB);
|
||||
user.setFirstLogin(firstLogin);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
|
||||
public void saveUser(String username, String password, String role)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
saveUser(username, password, role, false);
|
||||
}
|
||||
|
||||
@@ -247,7 +249,7 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
public void updateUserSettings(String username, Map<String, String> updates)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
@@ -259,7 +261,7 @@ public class UserService implements UserServiceInterface {
|
||||
settingsMap.putAll(updates);
|
||||
user.setSettings(settingsMap);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,38 +282,45 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
public void changeUsername(User user, String newUsername)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException,
|
||||
IOException,
|
||||
SQLException,
|
||||
UnsupportedProviderException {
|
||||
if (!isUsernameValid(newUsername)) {
|
||||
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
||||
}
|
||||
user.setUsername(newUsername);
|
||||
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));
|
||||
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);
|
||||
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);
|
||||
userAuthority.setAuthority(newRole);
|
||||
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);
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
|
||||
public boolean isPasswordCorrect(User user, String currentPassword) {
|
||||
@@ -397,7 +406,8 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void syncCustomApiUser(String customApiKey) throws IOException {
|
||||
public void syncCustomApiUser(String customApiKey)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -414,14 +424,14 @@ public class UserService implements UserServiceInterface {
|
||||
user.setApiKey(customApiKey);
|
||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||
userRepository.save(user);
|
||||
databaseBackupHelper.exportDatabase();
|
||||
databaseService.exportDatabase();
|
||||
} else {
|
||||
// Update API key if it has changed
|
||||
User user = existingUser.get();
|
||||
if (!customApiKey.equals(user.getApiKey())) {
|
||||
user.setApiKey(customApiKey);
|
||||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
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
|
||||
@Conditional(H2SQLCondition.class)
|
||||
public class ScheduledTasks {
|
||||
|
||||
private final DatabaseBackupHelper databaseBackupService;
|
||||
private final DatabaseInterface databaseService;
|
||||
|
||||
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
|
||||
this.databaseBackupService = databaseBackupService;
|
||||
public ScheduledTasks(DatabaseInterface databaseService) {
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void performBackup() throws IOException {
|
||||
databaseBackupService.exportDatabase();
|
||||
public void performBackup() throws SQLException, UnsupportedProviderException {
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package stirling.software.SPDF.config.security.oauth2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
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.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
public class CustomOAuth2AuthenticationSuccessHandler
|
||||
@@ -97,10 +99,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||
}
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
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.Security.SAML2;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -109,7 +111,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
|
||||
@@ -11,13 +11,11 @@ import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CustomSaml2ResponseAuthenticationConverter
|
||||
implements Converter<ResponseToken, Saml2Authentication> {
|
||||
|
||||
@@ -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 org.eclipse.jetty.http.HttpStatus;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
|
||||
import stirling.software.SPDF.config.security.database.DatabaseService;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/api/v1/database")
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@Conditional(H2SQLCondition.class)
|
||||
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
|
||||
public class DatabaseController {
|
||||
|
||||
private final DatabaseBackupHelper databaseBackupHelper;
|
||||
private final DatabaseService databaseService;
|
||||
|
||||
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
|
||||
this.databaseBackupHelper = databaseBackupHelper;
|
||||
public DatabaseController(DatabaseService databaseService) {
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
@@ -57,7 +59,7 @@ public class DatabaseController {
|
||||
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
|
||||
try (InputStream in = file.getInputStream()) {
|
||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
|
||||
boolean importSuccess = databaseService.importDatabaseFromUI(tempTemplatePath);
|
||||
if (importSuccess) {
|
||||
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
|
||||
} else {
|
||||
@@ -77,21 +79,20 @@ public class DatabaseController {
|
||||
@GetMapping("/import-database-file/{fileName}")
|
||||
public String importDatabaseFromBackupUI(
|
||||
@Parameter(description = "Name of the file to import", required = true) @PathVariable
|
||||
String fileName)
|
||||
throws IOException {
|
||||
String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return "redirect:/database?error=fileNullOrEmpty";
|
||||
}
|
||||
// Check if the file exists in the backup list
|
||||
boolean fileExists =
|
||||
databaseBackupHelper.getBackupList().stream()
|
||||
databaseService.getBackupList().stream()
|
||||
.anyMatch(backup -> backup.getFileName().equals(fileName));
|
||||
if (!fileExists) {
|
||||
log.error("File {} not found in backup list", fileName);
|
||||
return "redirect:/database?error=fileNotFound";
|
||||
}
|
||||
log.info("Received file: {}", fileName);
|
||||
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
|
||||
if (databaseService.importDatabaseFromUI(fileName)) {
|
||||
log.info("File {} imported to database", fileName);
|
||||
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
|
||||
}
|
||||
@@ -110,7 +111,7 @@ public class DatabaseController {
|
||||
throw new IllegalArgumentException("File must not be null or empty");
|
||||
}
|
||||
try {
|
||||
if (databaseBackupHelper.deleteBackupFile(fileName)) {
|
||||
if (databaseService.deleteBackupFile(fileName)) {
|
||||
log.info("Deleted file: {}", fileName);
|
||||
} else {
|
||||
log.error("Failed to delete file: {}", fileName);
|
||||
@@ -135,7 +136,7 @@ public class DatabaseController {
|
||||
throw new IllegalArgumentException("File must not be null or empty");
|
||||
}
|
||||
try {
|
||||
Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
|
||||
Path filePath = databaseService.getBackupFilePath(fileName);
|
||||
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
|
||||
@@ -157,14 +158,9 @@ public class DatabaseController {
|
||||
+ " database management page.")
|
||||
@GetMapping("/createDatabaseBackup")
|
||||
public String createDatabaseBackup() {
|
||||
try {
|
||||
log.info("Starting database backup creation...");
|
||||
databaseBackupHelper.exportDatabase();
|
||||
log.info("Database backup successfully created.");
|
||||
} catch (IOException e) {
|
||||
log.error("Error creating database backup: {}", e.getMessage(), e);
|
||||
return "redirect:/database?error=" + e.getMessage();
|
||||
}
|
||||
log.info("Starting database backup creation...");
|
||||
databaseService.exportDatabase();
|
||||
log.info("Database backup successfully created.");
|
||||
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.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
|
||||
@@ -33,7 +34,8 @@ public class SettingsController {
|
||||
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||
.body(
|
||||
"Setting has already been set, To adjust please edit /config/settings.yml");
|
||||
"Setting has already been set, To adjust please edit "
|
||||
+ InstallationPathConfig.getSettingsPath());
|
||||
}
|
||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||
|
||||
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.Principal;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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.User;
|
||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "User", description = "User APIs")
|
||||
@@ -52,7 +54,7 @@ public class UserController {
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/register")
|
||||
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
|
||||
model.addAttribute("error", "Username already exists");
|
||||
return "register";
|
||||
@@ -74,7 +76,7 @@ public class UserController {
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes)
|
||||
throws IOException {
|
||||
throws IOException, SQLException, UnsupportedProviderException {
|
||||
if (!userService.isUsernameValid(newUsername)) {
|
||||
return new RedirectView("/account?messageType=invalidUsername", true);
|
||||
}
|
||||
@@ -117,7 +119,7 @@ public class UserController {
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (principal == null) {
|
||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||
}
|
||||
@@ -145,7 +147,7 @@ public class UserController {
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
RedirectAttributes redirectAttributes)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (principal == null) {
|
||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||
}
|
||||
@@ -166,7 +168,7 @@ public class UserController {
|
||||
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
|
||||
@PostMapping("/updateUserSettings")
|
||||
public String updateUserSettings(HttpServletRequest request, Principal principal)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Map<String, String[]> paramMap = request.getParameterMap();
|
||||
Map<String, String> updates = new HashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
@@ -188,7 +190,7 @@ public class UserController {
|
||||
@RequestParam(name = "authType") String authType,
|
||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||
boolean forceChange)
|
||||
throws IllegalArgumentException, IOException {
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!userService.isUsernameValid(username)) {
|
||||
return new RedirectView("/addUsers?messageType=invalidUsername", true);
|
||||
}
|
||||
@@ -232,7 +234,7 @@ public class UserController {
|
||||
@RequestParam(name = "username") String username,
|
||||
@RequestParam(name = "role") String role,
|
||||
Authentication authentication)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (!userOpt.isPresent()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
@@ -270,7 +272,7 @@ public class UserController {
|
||||
@PathVariable("username") String username,
|
||||
@RequestParam("enabled") boolean enabled,
|
||||
Authentication authentication)
|
||||
throws IOException {
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (!userOpt.isPresent()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
|
||||
@@ -13,6 +13,9 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
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.springframework.beans.factory.annotation.Autowired;
|
||||
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.ConvertToPdfRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.*;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@@ -62,14 +62,20 @@ public class ConvertImgPDFController {
|
||||
String singleOrMultiple = request.getSingleOrMultiple();
|
||||
String colorType = request.getColorType();
|
||||
String dpi = request.getDpi();
|
||||
|
||||
String pageNumbers = request.getPageNumbers();
|
||||
Path tempFile = null;
|
||||
Path tempOutputDir = null;
|
||||
Path tempPdfPath = null;
|
||||
byte[] result = null;
|
||||
|
||||
String[] pageOrderArr =
|
||||
(pageNumbers != null && !pageNumbers.trim().isEmpty())
|
||||
? pageNumbers.split(",")
|
||||
: new String[] {"all"};
|
||||
;
|
||||
try {
|
||||
byte[] pdfBytes = file.getBytes();
|
||||
// Load the input PDF
|
||||
byte[] newPdfBytes = rearrangePdfPages(file.getBytes(), pageOrderArr);
|
||||
|
||||
ImageType colorTypeResult = ImageType.RGB;
|
||||
if ("greyscale".equals(colorType)) {
|
||||
colorTypeResult = ImageType.GRAY;
|
||||
@@ -84,7 +90,7 @@ public class ConvertImgPDFController {
|
||||
|
||||
result =
|
||||
PdfUtils.convertFromPdf(
|
||||
pdfBytes,
|
||||
newPdfBytes,
|
||||
"webp".equalsIgnoreCase(imageFormat)
|
||||
? "png"
|
||||
: imageFormat.toUpperCase(),
|
||||
@@ -227,4 +233,46 @@ public class ConvertImgPDFController {
|
||||
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
|
||||
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 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.Role;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ApiDocService {
|
||||
|
||||
private String getApiDocsUrl() {
|
||||
String contextPath = servletContext.getContextPath();
|
||||
String port = SPdfApplication.getStaticPort();
|
||||
String port = SPDFApplication.getStaticPort();
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@@ -35,22 +34,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
@Tag(name = "Pipeline", description = "Pipeline APIs")
|
||||
public class PipelineController {
|
||||
|
||||
final String watchedFoldersDir = "./pipeline/watchedFolders/";
|
||||
|
||||
final String finishedFoldersDir = "./pipeline/finishedFolders/";
|
||||
|
||||
private final PipelineProcessor processor;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public PipelineController(
|
||||
PipelineProcessor processor,
|
||||
ApplicationProperties applicationProperties,
|
||||
ObjectMapper objectMapper) {
|
||||
public PipelineController(PipelineProcessor processor, ObjectMapper objectMapper) {
|
||||
this.processor = processor;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
@@ -25,6 +24,7 @@ import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.PipelineConfig;
|
||||
import stirling.software.SPDF.model.PipelineOperation;
|
||||
import stirling.software.SPDF.utils.FileMonitor;
|
||||
@@ -48,14 +48,12 @@ public class PipelineDirectoryProcessor {
|
||||
public PipelineDirectoryProcessor(
|
||||
ObjectMapper objectMapper,
|
||||
ApiDocService apiDocService,
|
||||
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
|
||||
@Qualifier("finishedFoldersDir") String finishedFoldersDir,
|
||||
PipelineProcessor processor,
|
||||
FileMonitor fileMonitor) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.apiDocService = apiDocService;
|
||||
this.watchedFoldersDir = watchedFoldersDir;
|
||||
this.finishedFoldersDir = finishedFoldersDir;
|
||||
this.watchedFoldersDir = InstallationPathConfig.getPipelineWatchedFoldersDir();
|
||||
this.finishedFoldersDir = InstallationPathConfig.getPipelineFinishedFoldersDir();
|
||||
this.processor = processor;
|
||||
this.fileMonitor = fileMonitor;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import io.github.pixee.security.ZipSecurity;
|
||||
|
||||
import jakarta.servlet.ServletContext;
|
||||
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.PipelineOperation;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
@@ -80,7 +80,7 @@ public class PipelineProcessor {
|
||||
|
||||
private String getBaseUrl() {
|
||||
String contextPath = servletContext.getContextPath();
|
||||
String port = SPdfApplication.getStaticPort();
|
||||
String port = SPDFApplication.getStaticPort();
|
||||
return "http://localhost:" + port + contextPath + "/";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,18 @@ package stirling.software.SPDF.controller.api.security;
|
||||
import java.awt.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.PostMapping;
|
||||
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 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.RedactionArea;
|
||||
import stirling.software.SPDF.pdf.TextFinder;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
import stirling.software.SPDF.utils.PdfUtils;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
import stirling.software.SPDF.utils.propertyeditor.StringToArrayListPropertyEditor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@@ -41,6 +50,120 @@ public class RedactController {
|
||||
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")
|
||||
@Operation(
|
||||
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.bouncycastle.cert.X509CertificateHolder;
|
||||
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.util.Store;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
@@ -163,6 +163,9 @@ public class AccountWebController {
|
||||
case "invalid_destination":
|
||||
erroroauth = "login.invalid_destination";
|
||||
break;
|
||||
case "relying_party_registration_not_found":
|
||||
erroroauth = "login.relyingPartyRegistrationNotFound";
|
||||
break;
|
||||
// Valid InResponseTo was not available from the validation context, unable to
|
||||
// evaluate
|
||||
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 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;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Database Management", description = "Database management and security APIs")
|
||||
public class DatabaseWebController {
|
||||
|
||||
private final DatabaseBackupHelper databaseBackupHelper;
|
||||
private final DatabaseService databaseService;
|
||||
|
||||
public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) {
|
||||
this.databaseBackupHelper = databaseBackupHelper;
|
||||
public DatabaseWebController(DatabaseService databaseService) {
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
@@ -34,9 +34,12 @@ public class DatabaseWebController {
|
||||
} else if (confirmed != null) {
|
||||
model.addAttribute("infoMessage", confirmed);
|
||||
}
|
||||
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
||||
List<FileInfo> backupList = databaseService.getBackupList();
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
@@ -34,8 +35,6 @@ import stirling.software.SPDF.service.SignatureService;
|
||||
@Slf4j
|
||||
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 UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
@@ -223,7 +222,9 @@ public class GeneralWebController {
|
||||
// Extract font names from classpath
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
// Extract font names from external directory
|
||||
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:" + InstallationPathConfig.getStaticPath() + "fonts/*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,12 @@ public class SecurityWebController {
|
||||
return "security/auto-redact";
|
||||
}
|
||||
|
||||
@GetMapping("/redact")
|
||||
public String redactForm(Model model) {
|
||||
model.addAttribute("currentPage", "redact");
|
||||
return "security/redact";
|
||||
}
|
||||
|
||||
@GetMapping("/add-password")
|
||||
@Hidden
|
||||
public String addPasswordForm(Model model) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
@@ -13,18 +15,23 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.PropertySource;
|
||||
import org.springframework.core.Ordered;
|
||||
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.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.EncodedResource;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||
@@ -33,11 +40,37 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "")
|
||||
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
||||
@Data
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@Slf4j
|
||||
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 Security security = new Security();
|
||||
private System system = new System();
|
||||
@@ -79,23 +112,6 @@ public class ApplicationProperties {
|
||||
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 {
|
||||
ALL("all"),
|
||||
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
|
||||
public static class InitialLogin {
|
||||
private String username;
|
||||
@@ -153,6 +186,7 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public Resource getSpCert() {
|
||||
if (spCert == null) return null;
|
||||
if (spCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
||||
} else {
|
||||
@@ -161,6 +195,7 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public Resource getidpCert() {
|
||||
if (idpCert == null) return null;
|
||||
if (idpCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||
} else {
|
||||
@@ -247,6 +282,42 @@ public class ApplicationProperties {
|
||||
private String tessdataDir;
|
||||
private Boolean enableAlphaFunctionality;
|
||||
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
|
||||
@@ -295,6 +366,7 @@ public class ApplicationProperties {
|
||||
private boolean enabled;
|
||||
@ToString.Exclude private String key;
|
||||
private int maxUsers;
|
||||
private boolean ssoAutoLogin;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
|
||||
@Data
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.Date;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Lob;
|
||||
import jakarta.persistence.Table;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -15,7 +14,7 @@ import lombok.Data;
|
||||
public class SessionEntity implements Serializable {
|
||||
@Id private String sessionId;
|
||||
|
||||
@Lob private String principalName;
|
||||
private String principalName;
|
||||
|
||||
private Date lastRequest;
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public class User implements Serializable {
|
||||
@ElementCollection
|
||||
@MapKeyColumn(name = "setting_key")
|
||||
@Lob
|
||||
@Column(name = "setting_value", columnDefinition = "CLOB")
|
||||
@Column(name = "setting_value", columnDefinition = "text")
|
||||
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
|
||||
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.
|
||||
|
||||
|
||||
@@ -21,6 +21,11 @@ public class ConvertToImageRequest extends PDFFile {
|
||||
allowableValues = {"single", "multiple"})
|
||||
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(
|
||||
description = "The color type of the output image(s)",
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class SignatureService {
|
||||
|
||||
private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
|
||||
private static final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
private final String SIGNATURE_BASE_PATH;
|
||||
private final String ALL_USERS_FOLDER = "ALL_USERS";
|
||||
|
||||
public SignatureService() {
|
||||
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
|
||||
}
|
||||
|
||||
public boolean hasAccessToFile(String username, String fileName) throws IOException {
|
||||
validateFileName(fileName);
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@@ -34,9 +35,7 @@ public class FileMonitor {
|
||||
* monitored, false otherwise
|
||||
*/
|
||||
@Autowired
|
||||
public FileMonitor(
|
||||
@Qualifier("watchedFoldersDir") String rootDirectory,
|
||||
@Qualifier("directoryFilter") Predicate<Path> pathFilter)
|
||||
public FileMonitor(@Qualifier("directoryFilter") Predicate<Path> pathFilter)
|
||||
throws IOException {
|
||||
this.newlyDiscoveredFiles = new HashSet<>();
|
||||
this.path2KeyMapping = new HashMap<>();
|
||||
@@ -44,7 +43,7 @@ public class FileMonitor {
|
||||
this.pathFilter = pathFilter;
|
||||
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
|
||||
this.watchService = FileSystems.getDefault().newWatchService();
|
||||
this.rootDir = Path.of(rootDirectory);
|
||||
this.rootDir = Path.of(InstallationPathConfig.getPipelineWatchedFoldersDir());
|
||||
}
|
||||
|
||||
private boolean shouldNotProcess(Path path) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.github.pixee.security.HostValidator;
|
||||
import io.github.pixee.security.Urls;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
|
||||
@Slf4j
|
||||
public class GeneralUtils {
|
||||
@@ -83,7 +84,7 @@ public class GeneralUtils {
|
||||
|
||||
// Allow only http and https protocols
|
||||
String protocol = url.getProtocol();
|
||||
if (!protocol.equals("http") && !protocol.equals("https")) {
|
||||
if (!"http".equals(protocol) && !"https".equals(protocol)) {
|
||||
return false; // Disallow other protocols
|
||||
}
|
||||
|
||||
@@ -220,32 +221,54 @@ public class GeneralUtils {
|
||||
throw new IllegalArgumentException("Invalid expression");
|
||||
}
|
||||
|
||||
int n = 0;
|
||||
while (true) {
|
||||
for (int n = 1; n <= maxValue; n++) {
|
||||
// Replace 'n' with the current value of n, correctly handling numbers before
|
||||
// 'n'
|
||||
String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
|
||||
String sanitizedExpression = sanitizeNFunction(expression, n);
|
||||
Double result = evaluator.evaluate(sanitizedExpression);
|
||||
|
||||
// Check if the result is null or not within bounds
|
||||
if (result == null || result <= 0 || result.intValue() > maxValue) {
|
||||
if (n != 0) break;
|
||||
} else {
|
||||
if (result == null) break;
|
||||
|
||||
if (result.intValue() > 0 && result.intValue() <= maxValue)
|
||||
results.add(result.intValue());
|
||||
}
|
||||
n++;
|
||||
}
|
||||
|
||||
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) {
|
||||
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
|
||||
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
|
||||
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
|
||||
// Now replace 'n' with its current value
|
||||
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) {
|
||||
List<Integer> partResult = new ArrayList<>();
|
||||
|
||||
@@ -320,7 +343,10 @@ public class GeneralUtils {
|
||||
|
||||
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
||||
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());
|
||||
DumperOptions yamlOptionssettingsYml =
|
||||
@@ -338,7 +364,7 @@ public class GeneralUtils {
|
||||
|
||||
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
|
||||
throws IOException {
|
||||
Path path = Paths.get("configs", "settings.yml");
|
||||
Path path = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
|
||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||
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.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@@ -64,7 +65,7 @@ public class PDFToFile {
|
||||
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
|
||||
|
||||
// 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
|
||||
fileName = pdfBaseName + "ToHtml.zip";
|
||||
|
||||
@@ -22,9 +22,11 @@ import org.apache.pdfbox.text.TextPosition;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
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.ReplaceAndInvert;
|
||||
|
||||
@Slf4j
|
||||
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||
|
||||
private String textColor;
|
||||
@@ -93,17 +95,17 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
|
||||
try {
|
||||
font = PDFontFactory.createFont(text.getFont().getCOSObject());
|
||||
} 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);
|
||||
}
|
||||
// if a character is not supported by font, then look for supported font
|
||||
try {
|
||||
byte[] bytes = font.encode(unicodeText);
|
||||
} catch (IOException io) {
|
||||
System.out.println("text could not be encoded ");
|
||||
log.info("text could not be encoded ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} catch (IllegalArgumentException ie) {
|
||||
System.out.println("text not supported by font ");
|
||||
log.info("text not supported by font ");
|
||||
font = checkSupportedFontForCharacter(unicodeText);
|
||||
} finally {
|
||||
// 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);
|
||||
return currentFont;
|
||||
} catch (IOException io) {
|
||||
System.out.println("text could not be encoded ");
|
||||
log.info("text could not be encoded ");
|
||||
} catch (IllegalArgumentException ie) {
|
||||
System.out.println("text not supported by font ");
|
||||
log.info("text not supported by font ");
|
||||
}
|
||||
}
|
||||
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.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
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
|
||||
|
||||
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.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<define name="LOG_PATH" class="stirling.software.SPDF.config.LogbackPropertyLoader" />
|
||||
|
||||
<!-- Console Appender -->
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
@@ -7,35 +9,30 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- Rolling File Appender -->
|
||||
<!-- Rolling File Appender for Auth Logs -->
|
||||
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/invalid-auths.log</file>
|
||||
<file>${LOG_PATH}/invalid-auths.log</file>
|
||||
<encoder>
|
||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||
</encoder>
|
||||
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- daily rollover and keep 7 days' worth of history -->
|
||||
<fileNamePattern>logs/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<fileNamePattern>${LOG_PATH}/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>1</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<!-- Rolling File Appender -->
|
||||
<!-- Rolling File Appender for General Logs -->
|
||||
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/info.log</file>
|
||||
<file>${LOG_PATH}/info.log</file>
|
||||
<encoder>
|
||||
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
|
||||
</encoder>
|
||||
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- daily rollover and keep 7 days' worth of history -->
|
||||
<fileNamePattern>logs/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>1</maxHistory>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
|
||||
<!-- Root Logger -->
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
@@ -43,10 +40,9 @@
|
||||
</root>
|
||||
|
||||
<!-- Specific Logger -->
|
||||
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" level="ERROR"
|
||||
additivity="false">
|
||||
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler"
|
||||
level="ERROR" additivity="false">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="AUTHLOG"/>
|
||||
</logger>
|
||||
|
||||
</configuration>
|
||||
</configuration>
|
||||
@@ -82,6 +82,7 @@ pages=صفحات
|
||||
loading=جارٍ التحميل...
|
||||
addToDoc=إضافة إلى المستند
|
||||
reset=إعداة ضبط
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=سياسة الخصوصية
|
||||
legal.terms=شروط الاستخدام
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=لم يتم العثور على الملف
|
||||
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
||||
database.failedImportFile=فشل استيراد الملف
|
||||
database.notSupported=This function is not available for your database connection.
|
||||
|
||||
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
|
||||
session.refreshPage=تحديث الصفحة
|
||||
@@ -474,6 +476,10 @@ home.autoRedact.title=حجب تلقائي
|
||||
home.autoRedact.desc=يحجب (يسود) النص في PDF تلقائيًا بناءً على النص المدخل
|
||||
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.desc=يستخرج الجداول من PDF ويحولها إلى CSV
|
||||
tableExtraxt.tags=CSV,استخراج الجدول,استخراج,تحويل
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=طلب غير صالح
|
||||
login.oauth2AccessDenied=تم رفض الوصول
|
||||
login.oauth2InvalidTokenResponse=استجابة الرمز المميز غير صالحة
|
||||
login.oauth2InvalidIdToken=رمز الهوية غير صالح
|
||||
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||
login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل الدخول حاليًا باستخدام اسم المستخدم هذا. يرجى الاتصال بالمسؤول.
|
||||
login.alreadyLoggedIn=لقد تسجل دخولًا إلى
|
||||
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
|
||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=حشو إضافي مخصص
|
||||
autoRedact.convertPDFToImageLabel=تحويل PDF إلى صورة PDF (يستخدم لإزالة النص خلف المربع)
|
||||
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.title=إظهار جافا سكريبت
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=إصلاح
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=تدرج الرمادي
|
||||
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
|
||||
pdfToImage.submit=تحويل
|
||||
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
|
||||
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
|
||||
|
||||
|
||||
#addPassword
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=تقطيع ملف PDF
|
||||
fileChooser.click=انقر هنا
|
||||
fileChooser.or=أو
|
||||
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Səhifələr
|
||||
loading=Yüklənir...
|
||||
addToDoc=Sənədə Əlavə Et
|
||||
reset=Sıfırla
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Məxfilik Siyasəti
|
||||
legal.terms=Qaydalar və Şərtlər
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=Fayl Tapılmadı
|
||||
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
|
||||
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.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
|
||||
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.desc=PDF-dən cədvəlləri CSV-ə çevirərək xaric edir
|
||||
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.oauth2InvalidTokenResponse=Etibarsız Token Cavabı
|
||||
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.alreadyLoggedIn=Siz artıq daxil olmusunuz
|
||||
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.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.title=Javascripti Göstər
|
||||
@@ -830,6 +862,8 @@ sign.first=İlk səhifə
|
||||
sign.last=Son səhifə
|
||||
sign.next=Növbəti səhifə
|
||||
sign.previous=Əvvəlki səhifə
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Bərpa Et
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Boz Tonlama
|
||||
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
|
||||
pdfToImage.submit=Çevir
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=PDF-i Ayır
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Страници
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Политика за поверителност
|
||||
legal.terms=Правила и условия
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=Файлът не е намерен
|
||||
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
|
||||
database.failedImportFile=Неуспешно импортиране на файл
|
||||
database.notSupported=This function is not available for your database connection.
|
||||
|
||||
session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
|
||||
session.refreshPage=Refresh Page
|
||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Автоматично редактиране
|
||||
home.autoRedact.desc=Автоматично редактира (зачернява) текст в PDF въз основа на въведен текст
|
||||
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.desc=Извлича таблици от PDF, като ги конвертира в CSV
|
||||
tableExtraxt.tags=CSV,извличане на таблица,извличане,конвертиране
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Невалидна заявка
|
||||
login.oauth2AccessDenied=Отказан достъп
|
||||
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
|
||||
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
|
||||
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||
login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
|
||||
login.alreadyLoggedIn=Вече сте влезли в
|
||||
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
|
||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Персонализирана допълните
|
||||
autoRedact.convertPDFToImageLabel=Преобразуване на PDF към PDF-изображение (използва се за премахване на текст зад полето)
|
||||
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.title=Покажи Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Поправи
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Скала на сивото
|
||||
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
|
||||
pdfToImage.submit=Преобразуване
|
||||
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
|
||||
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
|
||||
|
||||
|
||||
#addPassword
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Разделяне на PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Pàgines
|
||||
loading=Carregant...
|
||||
addToDoc=Afegeix al document
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Política de Privacitat
|
||||
legal.terms=Termes i condicions
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=Fitxer no trobat
|
||||
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
|
||||
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.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
|
||||
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.desc=Extreu taules d'un PDF convertint-les a CSV
|
||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Sol·licitud no vàlida
|
||||
login.oauth2AccessDenied=Accés denegat
|
||||
login.oauth2InvalidTokenResponse=Resposta de token no vàlida
|
||||
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.alreadyLoggedIn=Ja has iniciat sessió a
|
||||
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.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.title=Mostra Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparar
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Escala de Grisos
|
||||
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
|
||||
pdfToImage.submit=Converteix
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Divideix PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Strany
|
||||
loading=Načítání...
|
||||
addToDoc=Přidat do dokumentu
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Politika soukromí
|
||||
legal.terms=Podmínky použití
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=File not Found
|
||||
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
|
||||
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.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
|
||||
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.desc=Extrahuje tabulky z PDF a konvertuje je do formátu CSV
|
||||
tableExtraxt.tags=CSV,Extrakce tabulky,extrahovat,konvertovat
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Neplatný požadavek
|
||||
login.oauth2AccessDenied=Přístup zazobán
|
||||
login.oauth2InvalidTokenResponse=Neplatná odpověď tokenu
|
||||
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.alreadyLoggedIn=Jste již přihlášeni na
|
||||
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.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.title=Zobrazit JavaScript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Opravit
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Stupně šedi
|
||||
pdfToImage.blackwhite=Černobílý (Může dojít k ztrátě dat!)
|
||||
pdfToImage.submit=Převést
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Podělit se PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Sideantal
|
||||
loading=Laster...
|
||||
addToDoc=Tilføj til Dokument
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Vilkår og betingelser
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=Fil ikke fundet
|
||||
database.fileNullOrEmpty=Fil må ikke være null eller tom
|
||||
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.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
|
||||
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.desc=Udtrækker Tabeller fra en PDF og konverterer dem til CSV
|
||||
tableExtraxt.tags=CSV,Tabeludtrækning,udtræk,konvertér
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Ugyldig Anmodning
|
||||
login.oauth2AccessDenied=Adgang Nægtet
|
||||
login.oauth2InvalidTokenResponse=Ugyldigt Token Svar
|
||||
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.alreadyLoggedIn=Du er allerede logget ind på
|
||||
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.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.title=Vis Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparér
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Gråtone
|
||||
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
|
||||
pdfToImage.submit=Konvertér
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Splitter PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Seiten
|
||||
loading=Laden...
|
||||
addToDoc=In Dokument hinzufügen
|
||||
reset=Zurücksetzen
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Datenschutz
|
||||
legal.terms=AGB
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Datenbanksicherung erfolgreich
|
||||
database.fileNotFound=Datei nicht gefunden
|
||||
database.fileNullOrEmpty=Datei darf nicht null oder leer sein
|
||||
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.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
|
||||
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.desc=Tabelle aus PDF in CSV extrahieren
|
||||
tableExtraxt.tags=CSV,tabelle,extrahieren
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=ungültige Anfrage
|
||||
login.oauth2AccessDenied=Zugriff abgelehnt
|
||||
login.oauth2InvalidTokenResponse=Ungültige Token-Antwort
|
||||
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.alreadyLoggedIn=Sie sind bereits an
|
||||
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.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.title=Javascript anzeigen
|
||||
@@ -830,6 +862,8 @@ sign.first=Erste Seite
|
||||
sign.last=Letzte Seite
|
||||
sign.next=Nächste Seite
|
||||
sign.previous=Vorherige Seite
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparieren
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Graustufen
|
||||
pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!)
|
||||
pdfToImage.submit=Umwandeln
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=PDF teilen
|
||||
fileChooser.click=Klicken
|
||||
fileChooser.or=oder
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Σελίδες
|
||||
loading=Φόρτωση...
|
||||
addToDoc=Πρόσθεση στο Εκπομπώματο
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Πολιτική Προνομίους
|
||||
legal.terms=Φράσεις Υποχρεωτικότητας
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=File not Found
|
||||
database.fileNullOrEmpty=Το αρχείο δεν μπορεί να είναι τυχόν ή κενό.
|
||||
database.failedImportFile=Failed Import File
|
||||
database.notSupported=This function is not available for your database connection.
|
||||
|
||||
session.expired=Η σεζώνη σας υπάρξει παραγωγή. Πατήστε για να ανανεώσετε το πλήρωμα και δοκιμάστε ξανά.
|
||||
session.refreshPage=Refresh Page
|
||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Αυτόματο Μαύρισμα Κειμένου
|
||||
home.autoRedact.desc=Αυτόματη επεξεργασία (Μαύρισμα) κείμενου σε PDF με βάση το κείμενο εισαγωγής
|
||||
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.desc=Εξάγει πίνακες από PDF μετατρέποντάς το σε CSV
|
||||
tableExtraxt.tags=CSV,καταθέσεις-τάβλες,αποδιατύπωση,μετατροπή
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Αρχαίως απαιτήσεις πληροφορ
|
||||
login.oauth2AccessDenied=Πρόσβαση ορισμένες τοποθετήσεις
|
||||
login.oauth2InvalidTokenResponse=Απάντηση άρκετο ρόταμα
|
||||
login.oauth2InvalidIdToken=Λιδός όρκου μη αρκετός
|
||||
login.relyingPartyRegistrationNotFound=No relying party registration found
|
||||
login.userIsDisabled=Ο χρήστης είναι δευτεροβαθμιακά διακωμένος, το σύστημα άλλαξε τον καθώς βρεθεί. Παρακαλείστε τους αρχηγούς να επιβεβαιώσουν τη συμπεριφορά χρήστη.
|
||||
login.alreadyLoggedIn=Είστε ήδη σύνδεδες σε
|
||||
login.alreadyLoggedIn2=κατοχόι. Παρακαλώ δυσκέντρωση τους και προσπαθήστε ξανά.
|
||||
@@ -578,6 +585,31 @@ autoRedact.customPaddingLabel=Εμπνευσμένη Προσθήκη Ανάκρ
|
||||
autoRedact.convertPDFToImageLabel=Μετατροπή PDF σε PDF-Εικόνα (Χρησιμοποιείται για την αφαίρεση κειμένου πίσω από το πλαίσιο)
|
||||
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.title=Εμφάνιση Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Επιδιόρθωση
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Κλίμακα του γκρι
|
||||
pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!)
|
||||
pdfToImage.submit=Μετατροπή
|
||||
pdfToImage.info=Δεν είναι ιστάμενος Python. Είναι απαιτήτων για τη μετατροπή σε WebP.
|
||||
pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1)
|
||||
|
||||
|
||||
#addPassword
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Διαλύστε το PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=File not found
|
||||
database.fileNullOrEmpty=File must not be null or empty
|
||||
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.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
|
||||
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.desc=Extracts Tables from a PDF converting it to CSV
|
||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
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.alreadyLoggedIn=You are already logged in to
|
||||
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.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.title=Show Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Repair
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Greyscale
|
||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||
pdfToImage.submit=Convert
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=File not Found
|
||||
database.fileNullOrEmpty=File must not be null or empty
|
||||
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.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
|
||||
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.desc=Extracts Tables from a PDF converting it to CSV
|
||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
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.alreadyLoggedIn=You are already logged in to
|
||||
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.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.title=Show Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Repair
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Grayscale
|
||||
pdfToImage.blackwhite=Black and White (May lose data!)
|
||||
pdfToImage.submit=Convert
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Páginas
|
||||
loading=Cargando...
|
||||
addToDoc=Agregar al Documento
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Política de Privacidad
|
||||
legal.terms=Términos y Condiciones
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=Archivo no encontrado
|
||||
database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
|
||||
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.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
|
||||
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.desc=Extraer Tablas de un PDF convirtiéndolas a CSV
|
||||
tableExtraxt.tags=CSV,Extraer tabla,extraer,convertir
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Solicitud no válida
|
||||
login.oauth2AccessDenied=Acceso denegado
|
||||
login.oauth2InvalidTokenResponse=Respuesta de token no válida
|
||||
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.alreadyLoggedIn=Ya has iniciado sesión en
|
||||
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.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.title=Mostrar Javascript
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparar
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Escala de grises
|
||||
pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!)
|
||||
pdfToImage.submit=Convertir
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Dividir PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
|
||||
@@ -82,6 +82,7 @@ pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
apply=Apply
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -248,6 +249,7 @@ database.backupCreated=Database backup successful
|
||||
database.fileNotFound=File not Found
|
||||
database.fileNullOrEmpty=File must not be null or empty
|
||||
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.refreshPage=Refresh Page
|
||||
@@ -474,6 +476,10 @@ home.autoRedact.title=Auto Idatzi
|
||||
home.autoRedact.desc=Auto Idatzi testua pdf fitxategian sarrerako testuan oinarritua
|
||||
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.desc=Extracts Tables from a PDF converting it to CSV
|
||||
tableExtraxt.tags=CSV,Table Extraction,extract,convert
|
||||
@@ -561,6 +567,7 @@ login.oauth2invalidRequest=Invalid Request
|
||||
login.oauth2AccessDenied=Access Denied
|
||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||
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.alreadyLoggedIn=You are already logged in to
|
||||
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.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.title=Javascript erakutsi
|
||||
@@ -830,6 +862,8 @@ sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
sign.maintainRatio=Toggle maintain aspect ratio
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Konpondu
|
||||
@@ -1041,6 +1075,7 @@ pdfToImage.grey=Gris-eskala
|
||||
pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!)
|
||||
pdfToImage.submit=Bihurtu
|
||||
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
|
||||
@@ -1284,6 +1319,8 @@ splitByChapters.submit=Split PDF
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#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