Compare commits

..

2 Commits

Author SHA1 Message Date
Anthony Stirling
a4ddb5f1af Update build.yml 2024-12-25 21:44:34 +00:00
Anthony Stirling
d4882df837 Update build.yml 2024-12-25 21:44:11 +00:00
171 changed files with 2508 additions and 14234 deletions

View File

@@ -9,7 +9,6 @@ Front End:
- any-glob-to-any-file: 'src/main/resources/templates/**/*' - any-glob-to-any-file: 'src/main/resources/templates/**/*'
- any-glob-to-any-file: 'src/main/resources/static/**/*' - any-glob-to-any-file: 'src/main/resources/static/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/UI/**/*'
Java: Java:
- changed-files: - changed-files:
@@ -30,7 +29,6 @@ Security:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/BackupNotFoundException.java'
- any-glob-to-any-file: 'scripts/download-security-jar.sh' - any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: '.github/workflows/dependency-review.yml' - any-glob-to-any-file: '.github/workflows/dependency-review.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml' - any-glob-to-any-file: '.github/workflows/scorecards.yml'
@@ -51,17 +49,12 @@ Documentation:
Docker: Docker:
- changed-files: - changed-files:
- any-glob-to-any-file: '.github/workflows/build.yml'
- any-glob-to-any-file: '.github/workflows/push-docker.yml'
- any-glob-to-any-file: 'Dockerfile' - any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile.*' - any-glob-to-any-file: 'Dockerfile-*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml' - any-glob-to-any-file: 'exampleYmlFiles/*.yml'
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: 'scripts/init.sh' - any-glob-to-any-file: 'scripts/init.sh'
- any-glob-to-any-file: 'scripts/init-without-ocr.sh' - any-glob-to-any-file: 'scripts/init-without-ocr.sh'
- any-glob-to-any-file: 'scripts/installFonts.sh' - any-glob-to-any-file: 'scripts/installFonts.sh'
- any-glob-to-any-file: 'test.sh'
- any-glob-to-any-file: 'test2.sh'
Test: Test:
- changed-files: - changed-files:

View File

@@ -4,15 +4,9 @@ on:
issue_comment: issue_comment:
types: [created] types: [created]
permissions:
contents: read
jobs: jobs:
check-comment: check-comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: read
issues: read
if: | if: |
github.event.issue.pull_request && github.event.issue.pull_request &&
( (
@@ -26,8 +20,7 @@ jobs:
github.event.comment.user.login == 'Ludy87' || github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'LaserKaspar' || github.event.comment.user.login == 'LaserKaspar' ||
github.event.comment.user.login == 'sbplat' || github.event.comment.user.login == 'sbplat' ||
github.event.comment.user.login == 'reecebrowne' || github.event.comment.user.login == 'reecebrowne'
github.event.comment.user.login == 'DarioGii'
) )
outputs: outputs:
pr_number: ${{ steps.get-pr.outputs.pr_number }} pr_number: ${{ steps.get-pr.outputs.pr_number }}
@@ -75,9 +68,6 @@ jobs:
deploy-pr: deploy-pr:
needs: check-comment needs: check-comment
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps: steps:
- name: Harden Runner - name: Harden Runner
@@ -108,9 +98,7 @@ jobs:
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: | run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0

View File

@@ -4,8 +4,7 @@ on:
pull_request: pull_request:
types: [opened, synchronize, reopened, closed] types: [opened, synchronize, reopened, closed]
permissions: permissions: read-all
contents: read
env: env:
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets

View File

@@ -3,8 +3,7 @@ on:
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize]
permissions: permissions: read-all
contents: read
jobs: jobs:
labeler: labeler:

View File

@@ -6,15 +6,13 @@ on:
pull_request: pull_request:
branches: ["main"] branches: ["main"]
permissions: permissions: read-all
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read
security-events: write security-events: write
strategy: strategy:
@@ -47,17 +45,6 @@ jobs:
env: env:
DOCKER_ENABLE_SECURITY: true DOCKER_ENABLE_SECURITY: true
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: test-reports-jdk-${{ matrix.jdk-version }}
path: |
build/reports/tests/
build/test-results/
build/reports/problems/
retention-days: 3
docker-compose-tests: docker-compose-tests:
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' || # if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
# (github.event_name == 'pull_request' && # (github.event_name == 'pull_request' &&
@@ -95,7 +82,7 @@ jobs:
- name: Install Docker Compose - name: Install Docker Compose
run: | run: |
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 curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python - name: Set up Python
@@ -105,7 +92,7 @@ jobs:
- name: Pip requirements - name: Pip requirements
run: | run: |
pip install --require-hashes -r ./cucumber/requirements.txt pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests - name: Run Docker Compose Tests
run: | run: |

View File

@@ -6,16 +6,12 @@ on:
paths: paths:
- "src/main/resources/messages_*.properties" - "src/main/resources/messages_*.properties"
permissions: permissions: read-all
contents: read # Allow read access to repository content
jobs: jobs:
check-files: check-files:
if: github.event_name == 'pull_request_target' if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # Allow posting comments on issues/PRs
pull-requests: write
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
@@ -24,32 +20,26 @@ jobs:
- name: Checkout main branch first - name: Checkout main branch first
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Checkout PR branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
path: pr-branch
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: "3.12" python-version: "3.x"
- name: Get PR data - name: Install GitHub CLI
id: get-pr-data run: sudo apt-get update && sudo apt-get install -y gh
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const prNumber = context.payload.pull_request.number;
const repoOwner = context.payload.repository.owner.login;
const repoName = context.payload.repository.name;
const branch = context.payload.pull_request.head.ref;
console.log(`PR Number: ${prNumber}`);
console.log(`Repo Owner: ${repoOwner}`);
console.log(`Repo Name: ${repoName}`);
console.log(`Branch: ${branch}`);
core.setOutput("pr_number", prNumber);
core.setOutput("repo_owner", repoOwner);
core.setOutput("repo_name", repoName);
core.setOutput("branch", branch);
continue-on-error: true
- name: Fetch PR changed files - name: Fetch PR changed files
id: fetch-pr-changes id: fetch-pr-changes
@@ -57,125 +47,45 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
echo "Fetching PR changed files..." echo "Fetching PR changed files..."
cd pr-branch
gh repo set-default ${{ github.repository }}
# Store files in a safe way, only allowing valid properties files
echo "Getting list of changed files from PR..." echo "Getting list of changed files from PR..."
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 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
cd ..
- name: Determine reference file test echo "Processing changed files..."
mapfile -t CHANGED_FILES < changed_files.txt
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES_STR}"
- name: Determine reference file
id: determine-file id: determine-file
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 run: |
with: echo "Determining reference file..."
script: | if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
const fs = require("fs"); echo "Using PR branch reference file"
const path = require("path"); echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
else
echo "Using main branch reference file"
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
fi
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }}; - name: Show REFERENCE_FILE
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}"; run: echo "Reference file is set to ${REFERENCE_FILE}"
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
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 }}";
console.log(`Determining reference file for PR #${prNumber}`);
// Validate inputs
const validateInput = (input, regex, name) => {
if (!regex.test(input)) {
throw new Error(`Invalid ${name}: ${input}`);
}
};
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
// Get the list of changed files in the PR
const { data: files } = await github.rest.pulls.listFiles({
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
});
// Filter for relevant files based on the PR changes
const changedFiles = files
.map(file => file.filename)
.filter(file => /^src\/main\/resources\/messages_[a-zA-Z_]+\.properties$/.test(file));
console.log("Changed files:", changedFiles);
// Create a temporary directory for PR files
const tempDir = "pr-branch";
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Download and save each changed file
for (const file of changedFiles) {
const { data: fileContent } = await github.rest.repos.getContent({
owner: prRepoOwner,
repo: prRepoName,
path: file,
ref: branch,
});
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
const filePath = path.join(tempDir, file);
const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(filePath, content);
console.log(`Saved file: ${filePath}`);
}
// Output the list of changed files for further processing
const fileList = changedFiles.join(" ");
core.exportVariable("FILES_LIST", fileList);
console.log("Files saved and listed in FILES_LIST.");
// Determine reference file
let referenceFilePath;
if (changedFiles.includes("src/main/resources/messages_en_GB.properties")) {
console.log("Using PR branch reference file.");
const { data: fileContent } = await github.rest.repos.getContent({
owner: prRepoOwner,
repo: prRepoName,
path: "src/main/resources/messages_en_GB.properties",
ref: branch,
});
referenceFilePath = "pr-branch-messages_en_GB.properties";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
} else {
console.log("Using main branch reference file.");
const { data: fileContent } = await github.rest.repos.getContent({
owner: repoOwner,
repo: repoName,
path: "src/main/resources/messages_en_GB.properties",
ref: "main",
});
referenceFilePath = "main-branch-messages_en_GB.properties";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
}
console.log(`Reference file path: ${referenceFilePath}`);
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Run Python script to check files - name: Run Python script to check files
id: run-check id: run-check
run: | run: |
echo "Running Python script to check files..." echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \ python main-branch/.github/scripts/check_language_properties.py \
--actor ${{ github.event.pull_request.user.login }} \ --actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \ --reference-file "${REFERENCE_FILE}" \
--branch "pr-branch" \ --branch pr-branch \
--files "${FILES_LIST[@]}" > result.txt --files "${CHANGED_FILES[@]}" > result.txt || true
continue-on-error: true # Continue the job even if this step fails
- name: Capture output - name: Capture output
id: capture-output id: capture-output
@@ -188,7 +98,7 @@ jobs:
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
echo "${SCRIPT_OUTPUT}" echo "${SCRIPT_OUTPUT}"
# Determine job failure based on script output # Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
echo "FAIL_JOB=true" >> $GITHUB_ENV echo "FAIL_JOB=true" >> $GITHUB_ENV
else else
@@ -207,18 +117,18 @@ jobs:
script: | script: |
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const issueNumber = context.issue.number; const prNumber = context.issue.number;
// Find existing comment // Find existing comment
const comments = await github.rest.issues.listComments({ const comments = await github.rest.issues.listComments({
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
issue_number: issueNumber issue_number: prNumber
}); });
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary")); const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
// Only update or create comments by the action user // Only allow the action user to update comments
const expectedActor = "github-actions[bot]"; const expectedActor = "github-actions[bot]";
if (comment && comment.user.login === expectedActor) { if (comment && comment.user.login === expectedActor) {
@@ -235,7 +145,7 @@ jobs:
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
issue_number: issueNumber, issue_number: prNumber,
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n` body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
}); });
console.log("Created new comment."); console.log("Created new comment.");

View File

@@ -7,8 +7,7 @@ on:
paths: paths:
- "build.gradle" - "build.gradle"
permissions: permissions: read-all
contents: read
jobs: jobs:
generate-license-report: generate-license-report:
@@ -53,7 +52,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update 3rd Party Licenses" commit-message: "Update 3rd Party Licenses"

View File

@@ -4,8 +4,7 @@ on:
schedule: schedule:
- cron: "30 20 * * *" - cron: "30 20 * * *"
permissions: permissions: read-all
contents: read
jobs: jobs:
labeler: labeler:

View File

@@ -5,8 +5,7 @@ on:
release: release:
types: [created] types: [created]
permissions: permissions: read-all
contents: read
jobs: jobs:
build-installers: build-installers:
@@ -43,7 +42,7 @@ jobs:
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with: with:
gradle-version: 8.12 gradle-version: 8.7
# Install Windows dependencies # Install Windows dependencies
- name: Install WiX Toolset - name: Install WiX Toolset

View File

@@ -4,8 +4,7 @@ on:
push: push:
branches: [main] branches: [main]
permissions: permissions: read-all
contents: read
jobs: jobs:
update: update:
@@ -20,7 +19,7 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@v5
with: with:
python-version: 3.12 python-version: 3.12
- name: Run Pre-Commit Hooks - name: Run Pre-Commit Hooks
@@ -36,7 +35,7 @@ jobs:
git diff --staged --quiet || git commit -m ":file_folder: pre-commit git diff --staged --quiet || git commit -m ":file_folder: pre-commit
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes" > Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "ci: 🤖 format everything with pre-commit" commit-message: "ci: 🤖 format everything with pre-commit"

View File

@@ -9,13 +9,14 @@ on:
permissions: permissions:
contents: read contents: read
packages: write
id-token: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
packages: write packages: write
id-token: write
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2 uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
@@ -32,7 +33,7 @@ jobs:
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with: with:
gradle-version: 8.12 gradle-version: 8.7
- name: Run Gradle Command - name: Run Gradle Command
run: ./gradlew clean build run: ./gradlew clean build
@@ -40,8 +41,7 @@ jobs:
DOCKER_ENABLE_SECURITY: false DOCKER_ENABLE_SECURITY: false
- name: Install cosign - 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: with:
cosign-release: 'v2.4.1' cosign-release: 'v2.4.1'
@@ -105,19 +105,32 @@ jobs:
sbom: true sbom: true
- name: Sign regular images - name: Sign regular images
if: github.ref == 'refs/heads/master'
env: env:
DIGEST: ${{ steps.build-push-regular.outputs.digest }} DIGEST: ${{ steps.build-push-regular.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }} TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }} COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: | run: |
# Always sign images regardless of branch
echo "$TAGS" | tr ',' '\n' | while read -r tag; do echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --yes \ cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \ --key env://COSIGN_PRIVATE_KEY \
"${tag}@${DIGEST}" "${tag}@${DIGEST}"
done done
# For alpha builds specifically, we want to ensure they're marked as development builds
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "Signing alpha build with development attestation"
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
if [[ $tag == *":alpha" ]]; then
cosign attest --key env://COSIGN_PRIVATE_KEY \
--predicate <(echo '{"type":"development"}') \
--yes "${tag}@${DIGEST}"
fi
done
fi
- name: Generate tags ultra-lite - name: Generate tags ultra-lite
id: meta2 id: meta2
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
@@ -149,6 +162,18 @@ jobs:
provenance: true provenance: true
sbom: true sbom: true
- name: Sign ultra-lite images
if: github.ref != 'refs/heads/main'
env:
DIGEST: ${{ steps.build-push-lite.outputs.digest }}
TAGS: ${{ steps.meta2.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done
- name: Generate tags fat - name: Generate tags fat
id: meta3 id: meta3
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
@@ -182,7 +207,7 @@ jobs:
sbom: true sbom: true
- name: Sign fat images - name: Sign fat images
if: github.ref == 'refs/heads/master' if: github.ref != 'refs/heads/main'
env: env:
DIGEST: ${{ steps.build-push-fat.outputs.digest }} DIGEST: ${{ steps.build-push-fat.outputs.digest }}
TAGS: ${{ steps.meta3.outputs.tags }} TAGS: ${{ steps.meta3.outputs.tags }}

View File

@@ -5,8 +5,7 @@ on:
release: release:
types: [created] types: [created]
permissions: permissions: read-all
contents: read
jobs: jobs:
push: push:
@@ -38,7 +37,7 @@ jobs:
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with: with:
gradle-version: 8.12 gradle-version: 8.7
- name: Generate jar (With Security=${{ matrix.enable_security }}) - name: Generate jar (With Security=${{ matrix.enable_security }})
run: ./gradlew clean createExe run: ./gradlew clean createExe

View File

@@ -5,8 +5,7 @@ on:
- cron: "30 0 * * *" - cron: "30 0 * * *"
workflow_dispatch: workflow_dispatch:
permissions: permissions: read-all
contents: read
jobs: jobs:
stale: stale:

View File

@@ -6,8 +6,7 @@ on:
branches: branches:
- master - master
permissions: permissions: read-all
contents: read
jobs: jobs:
push: push:

View File

@@ -9,8 +9,7 @@ on:
- "src/main/resources/messages_*.properties" - "src/main/resources/messages_*.properties"
- "scripts/ignore_translation.toml" - "scripts/ignore_translation.toml"
permissions: permissions: read-all
contents: read
jobs: jobs:
sync-readme: sync-readme:
@@ -28,7 +27,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: "3.12" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: pip install tomlkit run: pip install tomlkit
- name: Sync README - name: Sync README
@@ -43,7 +42,7 @@ jobs:
git diff --staged --quiet || git commit -m ":memo: Sync README git diff --staged --quiet || git commit -m ":memo: Sync README
> Made via sync_files.yml" || echo "no changes" > Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files commit-message: Update files

View File

@@ -6,8 +6,7 @@ on:
paths: paths:
- "src/main/resources/messages_en_GB.properties" - "src/main/resources/messages_en_GB.properties"
permissions: permissions: read-all
contents: read
jobs: jobs:
update-translations-main: update-translations-main:
@@ -28,7 +27,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with: with:
python-version: "3.12" python-version: "3.x"
- name: Run Python script to check files - name: Run Python script to check files
id: run-check id: run-check
@@ -51,7 +50,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files" commit-message: "Update translation files"

20
.gitignore vendored
View File

@@ -146,34 +146,16 @@ out/
# cucumber # cucumber
/cucumber/reports/** /cucumber/reports/**
# Certs and Security Files # Certs
*.p12 *.p12
*.pk8
*.pem *.pem
*.crt *.crt
*.cer *.cer
*.cert
*.der *.der
*.key *.key
*.csr *.csr
*.kdbx
*.jks
*.asc
# SSH Keys
*.pub
*.priv
id_rsa
id_rsa.pub
id_ecdsa
id_ecdsa.pub
id_ed25519
id_ed25519.pub
.ssh/
*ssh
# cache # cache
.cache
.ruff_cache .ruff_cache
.mypy_cache .mypy_cache
.pytest_cache .pytest_cache

View File

@@ -35,9 +35,9 @@ Documentation for Stirling-PDF is handled in a separate repository. Please see [
First, make sure you've read the section [Pull Requests](#pull-requests). First, make sure you've read the section [Pull Requests](#pull-requests).
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec). To build from the source, please follow this [Guide](LocalRunGuide.md).
Developers should review our [Developer Guide](DeveloperGuide.md) If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
## License ## License

View File

@@ -1,5 +1,5 @@
# Build the application # Build the application
FROM gradle:8.12-jdk17 AS build FROM gradle:8.11-jdk17 AS build
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app

46
Endpoint-groups.md Normal file
View File

@@ -0,0 +1,46 @@
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| crop | ✔️ | | | | | | | | | ✔️ | | | |
| extract-page | ✔️ | | | | | | | | | ✔️ | | | |
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | |
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | | | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| file-to-pdf | | ✔️ | | | ✔️ | ✔️ | | ✔️ | | | | ✔️ | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | | | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | |
| remove-password | | | ✔️ | | | | | | | ✔️ | | | |
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | | | |
| add-image | | | | ✔️ | | | | | | ✔️ | | | |
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | | | |
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
| compare | | | | ✔️ | | | | | | | ✔️ | | |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
| sign | | | | ✔️ | | | | | | | ✔️ | | |

41
FolderScanning.md Normal file
View File

@@ -0,0 +1,41 @@
## User Guide for Local Directory Scanning and File Processing
### Setting Up Watched Folders
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
- The default directory for this is `./pipeline/watchedFolders/`.
- Place any directories you want to be scanned into this folder. This folder should contain multiple folders, each for their own tasks and pipelines.
### Configuring Processing with JSON Files
- In each directory you want processed (e.g., `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them). This can be made, configured, and downloaded from the Stirling-PDF Pipeline interface.
### Automatic Scanning and Processing
- The system automatically checks the watched folder every minute for new directories and files to process.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside according to the configuration.
### Processing Steps
- Files in each directory are processed according to the instructions in the JSON file.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to the next process.
### Results and Output
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
- Each processed file is named and organized according to the rules set in the JSON configuration.
### Completion and Cleanup
- Once processing is complete, the original files in the watched folder's directory are removed.
- You can find the processed files in the designated output location.
### Error Handling
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
### User Interaction
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
- The system handles the rest, including scanning, processing, and outputting results.

327
LocalRunGuide.md Normal file
View File

@@ -0,0 +1,327 @@
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. The amount of dependencies is to actually reduce overall size, i.e., installing LibreOffice subcomponents rather than the full LibreOffice package.
You could theoretically use a Distrobox/Toolbox if your distribution has old or not all packages. But you might just as well use the Docker container then.
### Step 1: Prerequisites
Install the following software, if not already installed:
- Java 17 or later (21 recommended)
- Gradle 7.0 or later (included within the repo, so not needed on the server)
- Git
- Python 3.8 (with pip)
- Make
- GCC/G++
- Automake
- Autoconf
- libtool
- pkg-config
- zlib1g-dev
- libleptonica-dev
For Debian-based systems, you can use the following command:
```bash
sudo apt-get update
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
```
For Fedora-based systems, use this command:
```bash
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip
```
For non-root users with Nix Package Manager, use the following command:
```bash
nix-channel --update
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
```
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
For Debian and Fedora, you can build it from source using the following commands:
```bash
mkdir ~/.git
cd ~/.git && \
git clone https://github.com/agl/jbig2enc.git && \
cd jbig2enc && \
./autogen.sh && \
./configure && \
make && \
sudo make install
```
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
```bash
nix-env -iA nixpkgs.jbig2enc
```
### Step 3: Install Additional Software
Next, we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
Install the following software:
- libreoffice-core
- libreoffice-common
- libreoffice-writer
- libreoffice-calc
- libreoffice-impress
- python3-uno
- unoconv
- pngquant
- unpaper
- qpdf
- opencv-python-headless
For Debian-based systems, you can use the following command:
```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
```
For Fedora:
```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
For Nix:
```bash
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
### Step 4: Clone and Build Stirling-PDF
```bash
cd ~/.git && \
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && \
cd Stirling-PDF && \
chmod +x ./gradlew && \
./gradlew build
```
### Step 5: Move Jar to Desired Location
After the build process, a `.jar` file will be generated in the `build/libs` directory. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. This folder is required for the Python scripts using OpenCV.
```bash
sudo mkdir /opt/Stirling-PDF && \
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ && \
sudo mv scripts /opt/Stirling-PDF/ && \
echo "Scripts installed."
```
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
```bash
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
```
### Step 6: Other Files
#### OCR
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-English scanning.
##### Installing Language Packs
The easiest method is to use the language packs provided by your repositories. Skip the other steps if they are available.
**Manual:**
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
**Debian-based systems**, install languages with this command:
```bash
sudo apt update && \
# All languages
# sudo apt install -y 'tesseract-ocr-*'
# Find languages:
apt search tesseract-ocr-
# View installed languages:
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
```
**Fedora:**
```bash
# All languages
# sudo dnf install -y tesseract-langpack-*
# Find languages:
dnf search -C tesseract-langpack-
# View installed languages:
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
```
**Nix:**
```bash
nix-env -iA nixpkgs.tesseract
```
**Note:** Nix Package Manager pre-installs almost all the language packs when Tesseract is installed.
### Step 7: Run Stirling-PDF
Those who have pushed to the root directory, run the following commands:
```bash
./gradlew bootRun
or
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
```
Since LibreOffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
```
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory /run/user/1501: Permission denied
```
To resolve this, before starting Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
```bash
mkdir temp
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
./gradlew bootRun
or
java -jar ./Stirling-PDF-*.jar
```
### Step 8: Adding a Desktop Icon
This will add a modified app starter to your app menu.
```bash
location=$(pwd)/gradlew
image=$(pwd)/docs/stirling-transparent.svg
cat > ~/.local/share/applications/Stirling-PDF.desktop <<EOF
[Desktop Entry]
Name=Stirling PDF;
GenericName=Launch StirlingPDF and open its WebGUI;
Category=Office;
Exec=xdg-open http://localhost:8080 && nohup $location bootRun &;
Icon=$image;
Keywords=pdf;
Type=Application;
NoDisplay=false;
Terminal=true;
EOF
```
Note: Currently, the app will run in the background until it is manually closed.
### Optional: Changing the Host and Port of the Application
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
```yaml
server:
host: 0.0.0.0 # Not working - use instead address
address: 0.0.0.0
port: 3000
```
`-Djava.net.preferIPv4Stack=true` --> To force IPv4 only in the Java starting command
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
### Optional: Run Stirling-PDF as a Service (requires root)
First, create a `.env` file, where you can store environment variables:
```bash
touch /opt/Stirling-PDF/.env
```
In this file, you can add all variables, one variable per line, as stated in the main readme (for example `SYSTEM_DEFAULTLOCALE="de-DE"`).
Create a new file where we store our service settings and open it with the nano editor:
```bash
nano /etc/systemd/system/stirlingpdf.service
```
Paste this content, and make sure to update the filename of the jar file. Press `Ctrl+S` and `Ctrl+X` to save and exit the nano editor:
```ini
[Unit]
Description=Stirling-PDF service
After=syslog.target network.target
[Service]
SuccessExitStatus=143
User=root
Group=root
Type=simple
EnvironmentFile=/opt/Stirling-PDF/.env
WorkingDirectory=/opt/Stirling-PDF
ExecStart=/usr/bin/java -jar Stirling-PDF-0.17.2.jar
ExecStop=/bin/kill -15 $MAINPID
[Install]
WantedBy=multi-user.target
```
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
```bash
sudo systemctl daemon-reload
```
Enable the service to tell it to start automatically:
```bash
sudo systemctl enable stirlingpdf.service
```
See the status of the service:
```bash
sudo systemctl status stirlingpdf.service
```
Manually start/stop/restart the service:
```bash
sudo systemctl start stirlingpdf.service
sudo systemctl stop stirlingpdf.service
sudo systemctl restart stirlingpdf.service
```
---
Remember to set the necessary environment variables before running the project if you want to customize the application. The list can be seen in the main readme.
You can do this in the terminal by using the `export` command or `-D` argument to the Java `-jar` command:
```bash
export APP_HOME_NAME="Stirling PDF"
or
-DAPP_HOME_NAME="Stirling PDF"

42
PipelineFeature.md Normal file
View File

@@ -0,0 +1,42 @@
# Pipeline Configuration and Usage Tutorial
- Configure the pipeline config file and input files to run files against it.
- For reuse, download the config file and re-upload it when needed, or place it in `/pipeline/defaultWebUIConfigs/` to auto-load in the web UI for all users.
## Steps to Configure and Use Your Pipeline
1. **Access Configuration**
- Upon entering the screen, click on the **Configure** button.
2. **Enter Pipeline Name**
- Provide a name for your pipeline in the designated field.
3. **Select Operations**
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
4. **Configure Operation Settings**
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
5. **Add More Operations**
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
6. **Save Settings**
- Click **Save Operation Settings** after customizing settings for each operation.
7. **Validate Pipeline**
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
8. **Download Pipeline Configuration**
- To use the configuration for folder scanning (or save it for future use and re-upload it), download a JSON file in this menu. You can also pre-load it for future use by placing it in `/pipeline/defaultWebUIConfigs/`. It will then appear in the dropdown menu for all users to use.
9. **Submit Files for Processing**
- If your pipeline is correctly set up, close the configure menu, input the files, and hit **Submit**.
10. **Note on Web UI Limitations**
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
### Current Limitations
- Cannot have more than one of the same operation.
- Cannot input additional files via UI.
- All files and operations run in serial mode.

377
README.md
View File

@@ -4,7 +4,6 @@
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Stirling-Tools/Stirling-PDF/badge)](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](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&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@@ -14,23 +13,18 @@
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point. All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
Homepage: [https://stirlingpdf.com](https://stirlingpdf.com)
All documentation available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
![stirling-home](images/stirling-home.jpg) ![stirling-home](images/stirling-home.jpg)
## Features ## Features
- 50+ PDF Operations - Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Parallel file processing and downloads
- Dark mode support - Dark mode support
- Custom download options - Custom download options
- Custom 'Pipelines' to run multiple features in a automated queue - Parallel file processing and downloads
- Custom 'Pipelines' to run multiple features in a queue
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation) - Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
- Enterprise features like SSO see [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
## PDF Features ## PDF Features
@@ -96,20 +90,97 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- Get all information on a PDF to view or export as JSON - Get all information on a PDF to view or export as JSON
- Show/detect embedded JavaScript - Show/detect embedded JavaScript
For an overview of the tasks and the technology each uses, please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
A demo of the app is available [here](https://stirlingpdf.io).
## Technologies Used
# 📖 Get Started - Spring Boot + Thymeleaf
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [qpdf](https://github.com/qpdf/qpdf)
- HTML, CSS, JavaScript
- Docker
- [PDF.js](https://github.com/mozilla/pdf.js)
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for: ## How to Use
- Installation guides for all platforms ### Windows
- Configuration options
- Feature documentation
- API reference
- Security setup
- Enterprise features
For Windows users, download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe).
### Locally
Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md).
### Docker / Podman
> [!NOTE]
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people who don't mind space optimization, just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-fat?label=Stirling-PDF%20Fat)
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
### Docker Run
```bash
docker run -d \
-p 8080:8080 \
-v ./trainingData:/usr/share/tessdata \
-v ./extraConfigs:/configs \
-v ./logs:/logs \
# Optional customization (not required)
# -v /location/of/customFiles:/customFiles \
-e DOCKER_ENABLE_SECURITY=false \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \
--name stirling-pdf \
stirlingtools/stirling-pdf:latest
```
### Docker Compose
```yaml
version: '3.3'
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:latest
ports:
- '8080:8080'
volumes:
- ./trainingData:/usr/share/tessdata # Required for extra OCR languages
- ./extraConfigs:/configs
# - ./customFiles:/customFiles/
# - ./logs:/logs/
environment:
- DOCKER_ENABLE_SECURITY=false
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
- LANGS=en_GB
```
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
### Kubernetes
See the kubernetes helm chart [here](https://github.com/Stirling-Tools/Stirling-PDF-chart)
## Enable OCR/Compression Feature
Please view the [HowToUseOCR.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md).
## Reuse Stored Files
Certain functionality like `Sign` supports pre-saved files stored at `/customFiles/signatures/`. Image files placed here will be accessible via the web UI. Currently, this supports two folder types:
- `/customFiles/signatures/ALL_USERS`: Accessible to all users, useful for organizations where many users use the same files or for users not using authentication
- `/customFiles/signatures/{username}`: Such as `/customFiles/signatures/froodle`, accessible only to the `froodle` username, private for all others
## Supported Languages ## Supported Languages
@@ -117,58 +188,236 @@ Stirling-PDF currently supports 38 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![91%](https://geps.dev/progress/91) | | Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![89%](https://geps.dev/progress/89) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![92%](https://geps.dev/progress/92) |
| Basque (Euskara) (eu_ES) | ![51%](https://geps.dev/progress/51) | | Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) |
| Bulgarian (Български) (bg_BG) | ![87%](https://geps.dev/progress/87) | | Bulgarian (Български) (bg_BG) | ![89%](https://geps.dev/progress/89) |
| Catalan (Català) (ca_CA) | ![81%](https://geps.dev/progress/81) | | Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) |
| Croatian (Hrvatski) (hr_HR) | ![88%](https://geps.dev/progress/88) | | Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) |
| Czech (Česky) (cs_CZ) | ![88%](https://geps.dev/progress/88) | | Czech (Česky) (cs_CZ) | ![90%](https://geps.dev/progress/90) |
| Danish (Dansk) (da_DK) | ![87%](https://geps.dev/progress/87) | | Danish (Dansk) (da_DK) | ![89%](https://geps.dev/progress/89) |
| Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) | | Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) | | French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) |
| German (Deutsch) (de_DE) | ![96%](https://geps.dev/progress/96) | | German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) |
| Greek (Ελληνικά) (el_GR) | ![87%](https://geps.dev/progress/87) | | Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) |
| Hindi (हिंदी) (hi_IN) | ![85%](https://geps.dev/progress/85) | | Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) |
| Hungarian (Magyar) (hu_HU) | ![88%](https://geps.dev/progress/88) | | Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![88%](https://geps.dev/progress/88) | | Indonesian (Bahasa Indonesia) (id_ID) | ![90%](https://geps.dev/progress/90) |
| Irish (Gaeilge) (ga_IE) | ![80%](https://geps.dev/progress/80) | | Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) | | Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![86%](https://geps.dev/progress/86) | | Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Norwegian (Norsk) (no_NB) | ![80%](https://geps.dev/progress/80) | | Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) |
| Persian (فارسی) (fa_IR) | ![95%](https://geps.dev/progress/95) | | Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) |
| Polish (Polski) (pl_PL) | ![87%](https://geps.dev/progress/87) | | Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese (Português) (pt_PT) | ![87%](https://geps.dev/progress/87) | | Portuguese (Português) (pt_PT) | ![90%](https://geps.dev/progress/90) |
| Portuguese Brazilian (Português) (pt_BR) | ![95%](https://geps.dev/progress/95) | | Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![82%](https://geps.dev/progress/82) | | Romanian (Română) (ro_RO) | ![84%](https://geps.dev/progress/84) |
| Russian (Русский) (ru_RU) | ![87%](https://geps.dev/progress/87) | | Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![64%](https://geps.dev/progress/64) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) |
| Simplified Chinese (简体中文) (zh_CN) | ![90%](https://geps.dev/progress/90) | | Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![75%](https://geps.dev/progress/75) | | Slovakian (Slovensky) (sk_SK) | ![78%](https://geps.dev/progress/78) |
| Spanish (Español) (es_ES) | ![88%](https://geps.dev/progress/88) | | Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) |
| Swedish (Svenska) (sv_SE) | ![88%](https://geps.dev/progress/88) | | Swedish (Svenska) (sv_SE) | ![90%](https://geps.dev/progress/90) |
| Thai (ไทย) (th_TH) | ![87%](https://geps.dev/progress/87) | | Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![96%](https://geps.dev/progress/96) | | Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) | | Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) |
| Turkish (Türkçe) (tr_TR) | ![83%](https://geps.dev/progress/83) | | Ukrainian (Українська) (uk_UA) | ![76%](https://geps.dev/progress/76) |
| Ukrainian (Українська) (uk_UA) | ![73%](https://geps.dev/progress/73) | | Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![80%](https://geps.dev/progress/80) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
Please see our [Contributing Guide](CONTRIBUTING.md).
## Stirling PDF Enterprise ## Stirling PDF Enterprise
Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features, support and comforts. Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features and comforts.
Check out our [Enterprise docs](https://docs.stirlingpdf.com/Enterprise%20Edition)
### What's included
## 🤝 Looking to contribute? - Prioritized Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from the same email domain registered with us)
- Prioritised Enhancements to Stirling-PDF where applicable
- Base SSO support
- Advanced SSO such as automated login handling (Coming very soon)
- SAML SSO (Coming very soon)
- Custom automated metadata handling
- Advanced user configurations (Coming soon)
- Plus other exciting features to come
Join our community: Check out our [docs](https://docs.stirlingpdf.com/Enterprise%20Edition) on it or our official [website](https://www.stirlingpdf.com)
- [Contribution Guidelines](CONTRIBUTING.md)
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md) ## Customization
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
- [Discord Community](https://discord.gg/HYmhKj45pU) Stirling-PDF allows easy customization of the app, including things like:
- [Developer Guide](DeveloperGuide.md)
- Custom application name
- Custom slogans, icons, HTML, images, CSS, etc. (via file overrides)
There are two options for this, either using the generated settings file `settings.yml`, which is located in the `/configs` directory and follows standard YAML formatting, or using environment variables, which would override the settings file.
For example, in `settings.yml`, you might have:
```yaml
security:
enableLogin: 'true'
```
To have this via an environment variable, you would use `SECURITY_ENABLELOGIN`.
The current list of settings is:
```yaml
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
initialLogin:
username: '' # initial username for the first login
password: '' # initial password for the first login
oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client:
keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2
google:
clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2
github:
clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:okta.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
system:
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
ui:
appName: '' # application's visible name
homeDescription: '' # short description or tagline shown on the homepage
appNameNavbar: '' # name displayed on the navigation bar
endpoints:
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: example
UUID: example
```
There is an additional config file `/configs/custom_settings.yml` where users familiar with Java and Spring `application.properties` can input their own settings on top of Stirling-PDF's existing ones.
### Extra Notes
- **Endpoints**: Currently, the `ENDPOINTS_TO_REMOVE` and `GROUPS_TO_REMOVE` endpoints can include comma-separated lists of endpoints and groups to disable. For example, `ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages` would disable both image-to-pdf and remove pages, while `GROUPS_TO_REMOVE=LibreOffice` would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
- **customStaticFilePath**: Customize static files such as the app logo by placing files in the `/customFiles/static/` directory. An example of customizing the app logo is placing `/customFiles/static/favicon.svg` to override the current SVG. This can be used to change any `images/icons/css/fonts/js`, etc. in Stirling-PDF.
### Environment-Only Parameters
- `SYSTEM_ROOTURIPATH` - Set the application's root URI (e.g. `/pdf-app` to set the root URI to `localhost:8080/pdf-app`)
- `SYSTEM_CONNECTIONTIMEOUTMINUTES` - Set custom connection timeout values
- `DOCKER_ENABLE_SECURITY` - Set to `true` to download security jar (required for authentication login)
- `INSTALL_BOOK_AND_ADVANCED_HTML_OPS` - Download Calibre onto Stirling-PDF to enable PDF to/from book and advanced HTML conversion
- `LANGS` - Define custom font libraries to install for document conversions
## API
For those wanting to use Stirling-PDF's backend API to link with their own custom scripting to edit PDFs, you can view all existing API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/), or navigate to `/swagger-ui/index.html` of your Stirling-PDF instance for your version's documentation (or by following the API button in the settings of Stirling-PDF).
## Login Authentication
![stirling-login](images/login-light.png)
### Prerequisites
- User must have the folder `./configs` volumed within Docker so that it is retained during updates.
- Docker users must download the security jar version by setting `DOCKER_ENABLE_SECURITY` to `true` in environment variables.
- Then either enable login via the `settings.yml` file or set `SECURITY_ENABLE_LOGIN` to `true`.
- Now the initial user will be generated with username `admin` and password `stirling`. On login, you will be forced to change the password to a new one. You can also use the environment variables `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` to set your own credentials straight away (recommended to remove them after user creation).
Once the above has been done, on restart, a new `stirling-pdf-DB.mv.db` will show if everything worked.
When you log in to Stirling-PDF, you will be redirected to the `/login` page to log in with those default credentials. After login, everything should function as normal.
To access your account settings, go to Account Settings in the settings cog menu (top right in the navbar). This Account Settings menu is also where you find your API key.
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here, you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user.
## FAQ
### Q1: What are your planned features?
- Progress bar/tracking
- Full custom logic pipelines to combine multiple operations together
- Folder support with auto-scanning to perform operations on
- Redact text (via UI, not just automated)
- Add forms
- Multi-page layout (stitch PDF pages together) support x rows y columns and custom page sizing
- Fill forms manually or automatically
### Q2: Why is my application downloading .htm files? Why am I getting HTTP error 413?
This is an issue commonly caused by your NGINX configuration. The default file upload size for NGINX is 1MB. You need to add the following in your Nginx sites-available file: `client_max_body_size SIZE;` (where "SIZE" is 50M, for example, for 50MB files).
### Q3: Why is my download timing out?
NGINX has timeout values by default, so if you are running Stirling-PDF behind NGINX, you may need to set a timeout value, such as adding the config `proxy_read_timeout 3600;`.

68
Version-groups.md Normal file
View File

@@ -0,0 +1,68 @@
All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
| Technology | Ultra-Lite | Full |
| ---------- | :--------: | :---: |
| Java | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ |
| Libre | | ✔️ |
| Python | | ✔️ |
| OpenCV | | ✔️ |
| qpdf | | ✔️ |
| Operation | Ultra-Lite | Full |
| ---------------------- | ---------- | ---- |
| add-page-numbers | ✔️ | ✔️ |
| add-password | ✔️ | ✔️ |
| add-image | ✔️ | ✔️ |
| add-watermark | ✔️ | ✔️ |
| adjust-contrast | ✔️ | ✔️ |
| auto-split-pdf | ✔️ | ✔️ |
| auto-redact | ✔️ | ✔️ |
| auto-rename | ✔️ | ✔️ |
| cert-sign | ✔️ | ✔️ |
| remove-cert-sign | ✔️ | ✔️ |
| crop | ✔️ | ✔️ |
| change-metadata | ✔️ | ✔️ |
| change-permissions | ✔️ | ✔️ |
| compare | ✔️ | ✔️ |
| extract-page | ✔️ | ✔️ |
| extract-images | ✔️ | ✔️ |
| flatten | ✔️ | ✔️ |
| get-info-on-pdf | ✔️ | ✔️ |
| img-to-pdf | ✔️ | ✔️ |
| markdown-to-pdf | ✔️ | ✔️ |
| merge-pdfs | ✔️ | ✔️ |
| multi-page-layout | ✔️ | ✔️ |
| overlay-pdf | ✔️ | ✔️ |
| pdf-organizer | ✔️ | ✔️ |
| pdf-to-csv | ✔️ | ✔️ |
| pdf-to-img | ✔️ | ✔️ |
| pdf-to-single-page | ✔️ | ✔️ |
| remove-pages | ✔️ | ✔️ |
| remove-password | ✔️ | ✔️ |
| rotate-pdf | ✔️ | ✔️ |
| sanitize-pdf | ✔️ | ✔️ |
| scale-pages | ✔️ | ✔️ |
| sign | ✔️ | ✔️ |
| show-javascript | ✔️ | ✔️ |
| split-by-size-or-count | ✔️ | ✔️ |
| split-pdf-by-sections | ✔️ | ✔️ |
| split-pdfs | ✔️ | ✔️ |
| compress-pdf | | ✔️ |
| extract-image-scans | | ✔️ |
| ocr-pdf | | ✔️ |
| pdf-to-pdfa | | ✔️ |
| remove-blanks | | ✔️ |
| pdf-to-text | ✔️ | ✔️ |
| pdf-to-html | | ✔️ |
| pdf-to-word | | ✔️ |
| pdf-to-presentation | | ✔️ |
| pdf-to-xml | | ✔️ |
| remove-annotations | ✔️ | ✔️ |
| remove-cert-sign | ✔️ | ✔️ |
| remove-image-pdf | ✔️ | ✔️ |
| file-to-pdf | | ✔️ |
| html-to-pdf | | ✔️ |
| url-to-pdf | | ✔️ |
| repair | | ✔️ |

View File

@@ -27,7 +27,7 @@ ext {
} }
group = "stirling.software" group = "stirling.software"
version = "0.36.6" version = "0.36.5"
java { java {
@@ -52,15 +52,13 @@ sourceSets {
java { java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude "stirling/software/SPDF/controller/api/UserController.java" exclude "stirling/software/SPDF/controller/api/UserController.java"
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java" exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude "stirling/software/SPDF/controller/web/AccountWebController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
exclude "stirling/software/SPDF/model/AttemptCounter.java" exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude "stirling/software/SPDF/model/Authority.java" exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/BackupNotFoundException.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java" exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java" exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
@@ -71,29 +69,7 @@ sourceSets {
exclude "stirling/software/SPDF/UI/impl/**" exclude "stirling/software/SPDF/UI/impl/**"
} }
}
}
test {
java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/UserControllerTest.java"
exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java"
exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java"
exclude "stirling/software/SPDF/model/AttemptCounterTest.java"
exclude "stirling/software/SPDF/model/AuthorityTest.java"
exclude "stirling/software/SPDF/model/PersistentLoginTest.java"
exclude "stirling/software/SPDF/model/SessionEntityTest.java"
exclude "stirling/software/SPDF/model/UserTest.java"
exclude "stirling/software/SPDF/repository/**"
}
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
} }
} }
} }
@@ -147,13 +123,11 @@ jpackage {
windows { windows {
launcherAsService = false launcherAsService = false
appVersion = project.version appVersion = project.version
winConsole = false winConsole = false
winMenu = true // Creates start menu entry winDirChooser = true
winShortcut = true // Creates desktop shortcut winMenu = true
winShortcutPrompt = true // Lets user choose whether to create shortcuts winShortcut = true
winDirChooser = true // Allows users to choose installation directory winPerUserInstall = true
winPerUserInstall = false
winMenuGroup = "Stirling Software" winMenuGroup = "Stirling Software"
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF" winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
@@ -283,7 +257,7 @@ spotless {
// rules=['unused-dependency'] // rules=['unused-dependency']
// } // }
tasks.wrapper { tasks.wrapper {
gradleVersion = "8.12" gradleVersion = "8.7"
} }
//tasks.withType(JavaCompile) { //tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:deprecation" // options.compilerArgs << "-Xlint:deprecation"
@@ -323,12 +297,10 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$springBootVersion" implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation "org.springframework:spring-jdbc:6.2.1"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.3.232"
runtimeOnly "org.postgresql:postgresql:42.7.4"
constraints { constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion" implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"

View File

@@ -66,7 +66,7 @@ Feature: API Validation
| pageNumbers | file_count | | pageNumbers | file_count |
| 1,3,5-9 | 8 | | 1,3,5-9 | 8 |
| all | 20 | | all | 20 |
| 2n+1 | 10 | | 2n+1 | 11 |
| 3n | 7 | | 3n | 7 |

View File

@@ -1,5 +0,0 @@
behave
requests
PyPDF2
reportlab
PyCryptodome

View File

@@ -1,255 +1,5 @@
# behave
# This file is autogenerated by pip-compile with Python 3.10 requests
# by the following command: PyPDF2
# reportlab
# pip-compile --generate-hashes --output-file='cucumber\requirements.txt' 'cucumber\requirements.in' PyCryptodome
#
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

View File

@@ -1,63 +0,0 @@
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

View File

@@ -14,9 +14,9 @@ services:
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw - /stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw - /stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw - /stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"

View File

@@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw - /stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw - /stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw - /stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true" SECURITY_ENABLELOGIN: "true"

View File

@@ -247,11 +247,6 @@ ignore = [
'showJS.tags', 'showJS.tags',
] ]
[zh_BO]
ignore = [
'language.direction',
]
[zh_CN] [zh_CN]
ignore = [ ignore = [
'language.direction', 'language.direction',

View File

@@ -27,9 +27,4 @@ public class EEAppConfig {
public boolean runningEnterpriseEdition() { public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getEnterpriseEnabledResult(); return licenseKeyChecker.getEnterpriseEnabledResult();
} }
@Bean(name = "SSOAutoLogin")
public boolean ssoAutoLogin() {
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
}
} }

View File

@@ -94,7 +94,7 @@ public class KeygenLicenseVerifier {
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug(" validateLicenseResponse body: " + response.body()); log.info(" validateLicenseResponse body: " + response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {

View File

@@ -18,7 +18,7 @@ public class LicenseKeyChecker {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private boolean enterpriseEnabledResult = false; private boolean enterpriseEnbaledResult = false;
@Autowired @Autowired
public LicenseKeyChecker( public LicenseKeyChecker(
@@ -35,12 +35,12 @@ public class LicenseKeyChecker {
private void checkLicense() { private void checkLicense() {
if (!applicationProperties.getEnterpriseEdition().isEnabled()) { if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnabledResult = false; enterpriseEnbaledResult = false;
} else { } else {
enterpriseEnabledResult = enterpriseEnbaledResult =
licenseService.verifyLicense( licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey()); applicationProperties.getEnterpriseEdition().getKey());
if (enterpriseEnabledResult) { if (enterpriseEnbaledResult) {
log.info("License key is valid."); log.info("License key is valid.");
} else { } else {
log.info("License key is invalid."); log.info("License key is invalid.");
@@ -55,6 +55,6 @@ public class LicenseKeyChecker {
} }
public boolean getEnterpriseEnabledResult() { public boolean getEnterpriseEnabledResult() {
return enterpriseEnabledResult; return enterpriseEnbaledResult;
} }
} }

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
@@ -25,17 +24,15 @@ import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
@EnableScheduling
@SpringBootApplication @SpringBootApplication
public class SPDFApplication { @EnableScheduling
@Slf4j
public class SPdfApplication {
private static String serverPortStatic;
private static String baseUrlStatic; private static String baseUrlStatic;
private static String serverPortStatic;
private final Environment env; private final Environment env;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final WebBrowser webBrowser; private final WebBrowser webBrowser;
@@ -43,7 +40,7 @@ public class SPDFApplication {
@Value("${baseUrl:http://localhost}") @Value("${baseUrl:http://localhost}")
private String baseUrl; private String baseUrl;
public SPDFApplication( public SPdfApplication(
Environment env, Environment env,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Autowired(required = false) WebBrowser webBrowser) { @Autowired(required = false) WebBrowser webBrowser) {
@@ -52,41 +49,42 @@ public class SPDFApplication {
this.webBrowser = webBrowser; this.webBrowser = webBrowser;
} }
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPDFApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
Properties props = new Properties(); Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false"); System.setProperty("java.awt.headless", "false");
app.setHeadless(false); app.setHeadless(false);
props.put("java.awt.headless", "false"); props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet"); props.put("spring.main.web-application-type", "servlet");
} }
app.setAdditionalProfiles("default");
app.setAdditionalProfiles(getActiveProfile(args)); app.addInitializers(new ConfigInitializer());
ConfigInitializer initializer = new ConfigInitializer();
try {
initializer.ensureConfigExists();
} catch (IOException | URISyntaxException e) {
log.error("Error initialising configuration", e);
}
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
// External config files // External config files
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath()); if (Files.exists(Paths.get("configs/settings.yml"))) {
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) { propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
propertyFiles.put(
"spring.config.additional-location",
"file:" + InstallationPathConfig.getSettingsPath());
} else { } else {
log.warn( log.warn("External configuration file 'configs/settings.yml' does not exist.");
"External configuration file '{}' does not exist.",
InstallationPathConfig.getSettingsPath());
} }
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
String existingLocation = String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", ""); propertyFiles.getOrDefault("spring.config.additional-location", "");
if (!existingLocation.isEmpty()) { if (!existingLocation.isEmpty()) {
@@ -94,39 +92,57 @@ public class SPDFApplication {
} }
propertyFiles.put( propertyFiles.put(
"spring.config.additional-location", "spring.config.additional-location",
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath()); existingLocation + "file:configs/custom_settings.yml");
} else { } else {
log.warn( log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
"Custom configuration file '{}' does not exist.",
InstallationPathConfig.getCustomSettingsPath());
} }
Properties finalProps = new Properties(); Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) { if (!propertyFiles.isEmpty()) {
finalProps.putAll( finalProps.putAll(
Collections.singletonMap( Collections.singletonMap(
"spring.config.additional-location", "spring.config.additional-location",
propertyFiles.get("spring.config.additional-location"))); propertyFiles.get("spring.config.additional-location")));
} }
if (!props.isEmpty()) { if (!props.isEmpty()) {
finalProps.putAll(props); finalProps.putAll(props);
} }
app.setDefaultProperties(finalProps); app.setDefaultProperties(finalProps);
app.run(args); app.run(args);
// Ensure directories are created // Ensure directories are created
try { try {
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath())); Files.createDirectories(Path.of("customFiles/static/"));
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath())); Files.createDirectories(Path.of("customFiles/templates/"));
} catch (Exception e) { } catch (Exception e) {
log.error("Error creating directories: {}", e.getMessage()); log.error("Error creating directories: {}", e.getMessage());
} }
printStartupLogs(); printStartupLogs();
} }
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port
"0";
} else {
SPdfApplication.serverPortStatic = port;
}
}
@PostConstruct @PostConstruct
public void init() { public void init() {
baseUrlStatic = this.baseUrl; baseUrlStatic = this.baseUrl;
@@ -157,17 +173,6 @@ public class SPDFApplication {
log.info("Running configs {}", applicationProperties.toString()); log.info("Running configs {}", applicationProperties.toString());
} }
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPDFApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
} else {
SPDFApplication.serverPortStatic = port;
}
}
@PreDestroy @PreDestroy
public void cleanup() { public void cleanup() {
if (webBrowser != null) { if (webBrowser != null) {
@@ -175,55 +180,10 @@ public class SPDFApplication {
} }
} }
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
private static String[] getActiveProfile(String[] args) {
if (args == null) {
return new String[] {"default"};
}
for (String arg : args) {
if (arg.contains("spring.profiles.active")) {
return arg.substring(args[0].indexOf('=') + 1).split(", ");
}
}
return new String[] {"default"};
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() { public String getNonStaticBaseUrl() {
return baseUrlStatic; return baseUrlStatic;
} }
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() { public String getNonStaticPort() {
return serverPortStatic; return serverPortStatic;
} }

View File

@@ -40,7 +40,6 @@ import me.friwi.jcefmaven.EnumProgress;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter; import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler; import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.InstallationPathConfig;
@Component @Component
@Slf4j @Slf4j
@@ -73,8 +72,7 @@ public class DesktopBrowser implements WebBrowser {
CefAppBuilder builder = new CefAppBuilder(); CefAppBuilder builder = new CefAppBuilder();
configureCefSettings(builder); configureCefSettings(builder);
builder.setProgressHandler(createProgressHandler()); builder.setProgressHandler(createProgressHandler());
builder.setInstallDir(
new File(InstallationPathConfig.getClientWebUIPath()));
// Build and initialize CEF // Build and initialize CEF
cefApp = builder.build(); cefApp = builder.build();
client = cefApp.createClient(); client = cefApp.createClient();
@@ -101,16 +99,8 @@ public class DesktopBrowser implements WebBrowser {
private void configureCefSettings(CefAppBuilder builder) { private void configureCefSettings(CefAppBuilder builder) {
CefSettings settings = builder.getCefSettings(); CefSettings settings = builder.getCefSettings();
String basePath = InstallationPathConfig.getClientWebUIPath(); settings.cache_path = new File("jcef-bundle").getAbsolutePath();
log.info("basePath " + basePath); settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
// settings.browser_subprocess_path = new File(basePath +
// "subprocess").getAbsolutePath();
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
settings.persist_session_cookies = true; settings.persist_session_cookies = true;
settings.windowless_rendering_enabled = false; settings.windowless_rendering_enabled = false;
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO; settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
@@ -222,9 +212,6 @@ public class DesktopBrowser implements WebBrowser {
} }
private void setupLoadHandler() { private void setupLoadHandler() {
final long initStartTime = System.currentTimeMillis();
log.info("Setting up load handler at: {}", initStartTime);
client.addLoadHandler( client.addLoadHandler(
new CefLoadHandlerAdapter() { new CefLoadHandlerAdapter() {
@Override @Override
@@ -233,77 +220,32 @@ public class DesktopBrowser implements WebBrowser {
boolean isLoading, boolean isLoading,
boolean canGoBack, boolean canGoBack,
boolean canGoForward) { boolean canGoForward) {
log.debug(
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
+ "browserInitialized: {}, Time elapsed: {}ms",
isLoading,
canGoBack,
canGoForward,
browserInitialized,
System.currentTimeMillis() - initStartTime);
if (!isLoading && !browserInitialized) { if (!isLoading && !browserInitialized) {
log.info(
"Browser finished loading, preparing to initialize UI components");
browserInitialized = true; browserInitialized = true;
SwingUtilities.invokeLater( SwingUtilities.invokeLater(
() -> { () -> {
try {
if (loadingWindow != null) { if (loadingWindow != null) {
log.info("Starting UI initialization sequence"); Timer timer =
new Timer(
// Close loading window first 500,
loadingWindow.setVisible(false); e -> {
loadingWindow.dispose(); loadingWindow.dispose();
loadingWindow = null; loadingWindow = null;
log.info("Loading window disposed");
// Then setup the main frame
frame.setVisible(false);
frame.dispose(); frame.dispose();
frame.setOpacity(1.0f); frame.setOpacity(1.0f);
frame.setUndecorated(false); frame.setUndecorated(false);
frame.pack(); frame.pack();
frame.setSize(1280, 800); frame.setSize(1280, 800);
frame.setLocationRelativeTo(null); frame.setLocationRelativeTo(null);
log.debug("Frame reconfigured");
// Show the main frame
frame.setVisible(true); frame.setVisible(true);
frame.requestFocus(); frame.requestFocus();
frame.toFront(); frame.toFront();
log.info("Main frame displayed and focused");
// Focus the browser component
Timer focusTimer =
new Timer(
100,
e -> {
try {
browser.getUIComponent() browser.getUIComponent()
.requestFocus(); .requestFocus();
log.info(
"Browser component focused");
} catch (Exception ex) {
log.error(
"Error focusing browser",
ex);
}
}); });
focusTimer.setRepeats(false); timer.setRepeats(false);
focusTimer.start(); timer.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();
}
} }
}); });
} }

View File

@@ -14,12 +14,9 @@ public class LoadingWindow extends JDialog {
private final JLabel statusLabel; private final JLabel statusLabel;
private final JPanel mainPanel; private final JPanel mainPanel;
private final JLabel brandLabel; private final JLabel brandLabel;
private long startTime;
public LoadingWindow(Frame parent, String initialUrl) { public LoadingWindow(Frame parent, String initialUrl) {
super(parent, "Initializing Stirling-PDF", true); super(parent, "Initializing Stirling-PDF", true);
startTime = System.currentTimeMillis();
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
// Initialize components // Initialize components
mainPanel = new JPanel(); mainPanel = new JPanel();
@@ -32,8 +29,8 @@ public class LoadingWindow extends JDialog {
gbc.gridwidth = GridBagConstraints.REMAINDER; gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL; gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5); gbc.insets = new Insets(5, 5, 5, 5);
gbc.weightx = 1.0; gbc.weightx = 1.0; // Add horizontal weight
gbc.weighty = 0.0; gbc.weighty = 0.0; // Add vertical weight
// Add icon // Add icon
try { try {
@@ -46,14 +43,12 @@ public class LoadingWindow extends JDialog {
iconLabel.setHorizontalAlignment(SwingConstants.CENTER); iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
gbc.gridy = 0; gbc.gridy = 0;
mainPanel.add(iconLabel, gbc); mainPanel.add(iconLabel, gbc);
log.debug("Icon loaded and scaled successfully");
} }
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to load icon", e); log.error("Failed to load icon", e);
} }
// URL Label with explicit size // URL Label with explicit size
brandLabel = new JLabel(initialUrl); brandLabel = new JLabel(initialUrl);
brandLabel.setHorizontalAlignment(SwingConstants.CENTER); brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
@@ -68,7 +63,6 @@ public class LoadingWindow extends JDialog {
statusLabel.setPreferredSize(new Dimension(300, 25)); statusLabel.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 2; gbc.gridy = 2;
mainPanel.add(statusLabel, gbc); mainPanel.add(statusLabel, gbc);
// Progress bar with explicit size // Progress bar with explicit size
progressBar = new JProgressBar(0, 100); progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true); progressBar.setStringPainted(true);
@@ -88,78 +82,33 @@ public class LoadingWindow extends JDialog {
setAlwaysOnTop(true); setAlwaysOnTop(true);
setProgress(0); setProgress(0);
setStatus("Starting..."); setStatus("Starting...");
log.info(
"LoadingWindow initialization completed in {}ms",
System.currentTimeMillis() - startTime);
} }
public void setProgress(final int progress) { public void setProgress(final int progress) {
SwingUtilities.invokeLater( SwingUtilities.invokeLater(
() -> { () -> {
try { try {
int validProgress = Math.min(Math.max(progress, 0), 100); progressBar.setValue(Math.min(Math.max(progress, 0), 100));
log.info( progressBar.setString(progress + "%");
"Setting progress to {}% at {}ms since start",
validProgress, System.currentTimeMillis() - startTime);
// Log additional details when near 90%
if (validProgress >= 85 && validProgress <= 95) {
log.info(
"Near 90% progress - Current status: {}, Window visible: {}, "
+ "Progress bar responding: {}, Memory usage: {}MB",
statusLabel.getText(),
isVisible(),
progressBar.isEnabled(),
Runtime.getRuntime().totalMemory() / (1024 * 1024));
// Add thread state logging
Thread currentThread = Thread.currentThread();
log.debug(
"Current thread state - Name: {}, State: {}, Priority: {}",
currentThread.getName(),
currentThread.getState(),
currentThread.getPriority());
}
progressBar.setValue(validProgress);
progressBar.setString(validProgress + "%");
mainPanel.revalidate(); mainPanel.revalidate();
mainPanel.repaint(); mainPanel.repaint();
} catch (Exception e) { } catch (Exception e) {
log.error("Error updating progress to " + progress, e); log.error("Error updating progress", e);
} }
}); });
} }
public void setStatus(final String status) { public void setStatus(final String status) {
log.info( log.info(status);
"Status update at {}ms - Setting status to: {}",
System.currentTimeMillis() - startTime,
status);
SwingUtilities.invokeLater( SwingUtilities.invokeLater(
() -> { () -> {
try { try {
String validStatus = status != null ? status : ""; statusLabel.setText(status != null ? status : "");
statusLabel.setText(validStatus);
// Log UI state when status changes
log.debug(
"UI State - Window visible: {}, Progress: {}%, Status: {}",
isVisible(), progressBar.getValue(), validStatus);
mainPanel.revalidate(); mainPanel.revalidate();
mainPanel.repaint(); mainPanel.repaint();
} catch (Exception e) { } catch (Exception e) {
log.error("Error updating status to: " + status, e); log.error("Error updating status", e);
} }
}); });
} }
@Override
public void dispose() {
log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime);
super.dispose();
}
} }

View File

@@ -136,6 +136,16 @@ public class AppConfig {
return false; return false;
} }
@Bean(name = "watchedFoldersDir")
public String watchedFoldersDir() {
return "./pipeline/watchedFolders/";
}
@Bean(name = "finishedFoldersDir")
public String finishedFoldersDir() {
return "./pipeline/finishedFolders/";
}
@Bean(name = "directoryFilter") @Bean(name = "directoryFilter")
public Predicate<Path> processOnlyFiles() { public Predicate<Path> processOnlyFiles() {
return path -> { return path -> {

View File

@@ -16,15 +16,27 @@ import org.simpleyaml.configuration.comments.CommentType;
import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation; import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions; import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class ConfigInitializer { public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
ensureConfigExists();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize application configuration", e);
}
}
public void ensureConfigExists() throws IOException, URISyntaxException { public void ensureConfigExists() throws IOException, URISyntaxException {
// Define the path to the external config directory // Define the path to the external config directory
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath()); Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists // Check if the file already exists
if (Files.notExists(destPath)) { if (Files.notExists(destPath)) {
@@ -41,11 +53,10 @@ public class ConfigInitializer {
"Resource file not found: settings.yml.template"); "Resource file not found: settings.yml.template");
} }
} }
log.info("Created settings file from template");
} else { } else {
// Define the path to the config settings file // Define the path to the config settings file
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); Path settingsPath = Paths.get("configs", "settings.yml");
// Load the template resource // Load the template resource
URL settingsTemplateResource = URL settingsTemplateResource =
getClass().getClassLoader().getResource("settings.yml.template"); getClass().getClassLoader().getResource("settings.yml.template");
@@ -109,7 +120,7 @@ public class ConfigInitializer {
} }
// Create custom settings file if it doesn't exist // Create custom settings file if it doesn't exist
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath()); Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
if (!Files.exists(customSettingsPath)) { if (!Files.exists(customSettingsPath)) {
Files.createFile(customSettingsPath); Files.createFile(customSettingsPath);
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -136,7 +135,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "remove-cert-sign"); addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "redact");
// Adding endpoints to "Other" group // Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "ocr-pdf");
@@ -236,7 +234,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "markdown-to-pdf"); addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "show-javascript"); addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "auto-redact"); addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Java", "redact");
addEndpointToGroup("Java", "pdf-to-csv"); addEndpointToGroup("Java", "pdf-to-csv");
addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "overlay-pdf");
@@ -268,14 +265,9 @@ public class EndpointConfiguration {
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
if (applicationProperties != null && applicationProperties.getEndpoints() != null) {
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (!bookAndHtmlFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
if (groupsToRemove == null) {
groupsToRemove = new ArrayList<>();
}
groupsToRemove.add("Calibre"); groupsToRemove.add("Calibre");
} }
if (endpointsToRemove != null) { if (endpointsToRemove != null) {
@@ -290,7 +282,6 @@ public class EndpointConfiguration {
} }
} }
} }
}
public Set<String> getEndpointsForGroup(String group) { public Set<String> getEndpointsForGroup(String group) {
return endpointGroups.getOrDefault(group, new HashSet<>()); return endpointGroups.getOrDefault(group, new HashSet<>());

View File

@@ -33,8 +33,7 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
String characterEncoding, String characterEncoding,
Map<String, Object> templateResolutionAttributes) { Map<String, Object> templateResolutionAttributes) {
Resource resource = Resource resource =
resourceLoader.getResource( resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
try { try {
if (resource.exists() && resource.isReadable()) { if (resource.exists() && resource.isReadable()) {
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding); return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);

View File

@@ -1,132 +0,0 @@
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;
}
}

View File

@@ -1,10 +0,0 @@
package stirling.software.SPDF.config;
import ch.qos.logback.core.PropertyDefinerBase;
public class LogbackPropertyLoader extends PropertyDefinerBase {
@Override
public String getPropertyValue() {
return InstallationPathConfig.getLogPath();
}
}

View File

@@ -23,8 +23,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations( .addResourceLocations("file:customFiles/static/", "classpath:/static/");
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
// .setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching
} }
} }

View File

@@ -16,6 +16,7 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
throws IOException { throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource()); factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject(); Properties properties = factory.getObject();
return new PropertiesPropertySource( return new PropertiesPropertySource(

View File

@@ -0,0 +1,17 @@
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();
}

View File

@@ -1,17 +0,0 @@
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();
}

View File

@@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils; import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// Construct URLs required for SAML configuration // Construct URLs required for SAML configuration
String serverUrl = String serverUrl =
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort(); SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
String relyingPartyIdentifier = String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId; serverUrl + "/saml2/service-provider-metadata/" + registrationId;

View File

@@ -1,56 +1,49 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.sql.SQLException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Slf4j
@Component @Component
@Slf4j
public class InitialSecuritySetup { public class InitialSecuritySetup {
private final UserService userService; private final UserService userService;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final DatabaseInterface databaseService; private final DatabaseBackupInterface databaseBackupHelper;
public InitialSecuritySetup( public InitialSecuritySetup(
UserService userService, UserService userService,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
DatabaseInterface databaseService) { DatabaseBackupInterface databaseBackupHelper) {
this.userService = userService; this.userService = userService;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.databaseService = databaseService; this.databaseBackupHelper = databaseBackupHelper;
} }
@PostConstruct @PostConstruct
public void init() { public void init() throws IllegalArgumentException, IOException {
try { if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
if (databaseService.hasBackup()) { databaseBackupHelper.importDatabase();
databaseService.importDatabase(); } else if (!userService.hasUsers()) {
}
if (!userService.hasUsers()) {
initializeAdminUser(); initializeAdminUser();
} } else {
databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO(); userService.migrateOauth2ToSSO();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.error("Failed to initialize security setup.", e);
System.exit(1);
} }
initializeInternalApiUser();
} }
private void initializeAdminUser() throws SQLException, UnsupportedProviderException { private void initializeAdminUser() throws IOException {
String initialUsername = String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = String initialPassword =
@@ -59,34 +52,36 @@ public class InitialSecuritySetup {
&& !initialUsername.isEmpty() && !initialUsername.isEmpty()
&& initialPassword != null && initialPassword != null
&& !initialPassword.isEmpty() && !initialPassword.isEmpty()
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) { && !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
log.info("Admin user created: {}", initialUsername); log.info("Admin user created: " + initialUsername);
} catch (IllegalArgumentException e) {
log.error("Failed to initialize security setup", e);
System.exit(1);
}
} else { } else {
createDefaultAdminUser(); createDefaultAdminUser();
} }
} }
private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException { private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
String defaultUsername = "admin"; String defaultUsername = "admin";
String defaultPassword = "stirling"; String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
log.info("Default admin user created: {}", defaultUsername); log.info("Default admin user created: " + defaultUsername);
} }
} }
private void initializeInternalApiUser() private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey()); userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
} }

View File

@@ -1,24 +1,39 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import org.springframework.beans.factory.annotation.Autowired; import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -28,16 +43,24 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter; import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
import stirling.software.SPDF.repository.PersistentLoginRepository; import stirling.software.SPDF.repository.PersistentLoginRepository;
@@ -49,7 +72,7 @@ import stirling.software.SPDF.repository.PersistentLoginRepository;
public class SecurityConfiguration { public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService; private final CustomUserDetailsService userDetailsService;
private final UserService userService; @Lazy private final UserService userService;
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
private final boolean loginEnabledValue; private final boolean loginEnabledValue;
@@ -63,10 +86,16 @@ public class SecurityConfiguration {
private final FirstLoginFilter firstLoginFilter; private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
private final PersistentLoginRepository persistentLoginRepository; private final PersistentLoginRepository persistentLoginRepository;
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
// // Only Dev test
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) ->
// web.ignoring()
// .requestMatchers(
// "/css/**", "/images/**", "/js/**", "/**.svg",
// "/pdfjs-legacy/**");
// }
public SecurityConfiguration( public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository, PersistentLoginRepository persistentLoginRepository,
CustomUserDetailsService userDetailsService, CustomUserDetailsService userDetailsService,
@@ -77,12 +106,7 @@ public class SecurityConfiguration {
UserAuthenticationFilter userAuthenticationFilter, UserAuthenticationFilter userAuthenticationFilter,
LoginAttemptService loginAttemptService, LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter, FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry, SessionPersistentRegistry sessionRegistry) {
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
this.userDetailsService = userDetailsService; this.userDetailsService = userDetailsService;
this.userService = userService; this.userService = userService;
this.loginEnabledValue = loginEnabledValue; this.loginEnabledValue = loginEnabledValue;
@@ -93,9 +117,6 @@ public class SecurityConfiguration {
this.firstLoginFilter = firstLoginFilter; this.firstLoginFilter = firstLoginFilter;
this.sessionRegistry = sessionRegistry; this.sessionRegistry = sessionRegistry;
this.persistentLoginRepository = persistentLoginRepository; this.persistentLoginRepository = persistentLoginRepository;
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
} }
@Bean @Bean
@@ -253,7 +274,7 @@ public class SecurityConfiguration {
userService, userService,
loginAttemptService)) loginAttemptService))
.userAuthoritiesMapper( .userAuthoritiesMapper(
oAuth2userAuthoritiesMapper)) userAuthoritiesMapper()))
.permitAll()); .permitAll());
} }
// Handle SAML // Handle SAML
@@ -270,7 +291,7 @@ public class SecurityConfiguration {
try { try {
saml2.loginPage("/saml2") saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository( .relyingPartyRegistrationRepository(
saml2RelyingPartyRegistrations) relyingPartyRegistrations())
.authenticationManager( .authenticationManager(
new ProviderManager(authenticationProvider)) new ProviderManager(authenticationProvider))
.successHandler( .successHandler(
@@ -281,7 +302,8 @@ public class SecurityConfiguration {
.failureHandler( .failureHandler(
new CustomSaml2AuthenticationFailureHandler()) new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver( .authenticationRequestResolver(
saml2AuthenticationRequestResolver); authenticationRequestResolver(
relyingPartyRegistrations()));
} catch (Exception e) { } catch (Exception e) {
log.error("Error configuring SAML2 login", e); log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e); throw new RuntimeException(e);
@@ -289,11 +311,244 @@ public class SecurityConfiguration {
}); });
} }
} else { } else {
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
// CookieCsrfTokenRepository cookieRepo =
// CookieCsrfTokenRepository.withHttpOnlyFalse();
// CsrfTokenRequestAttributeHandler requestHandler =
// new CsrfTokenRequestAttributeHandler();
// requestHandler.setCsrfRequestAttributeName(null);
// http.csrf(
// csrf ->
// csrf.csrfTokenRepository(cookieRepo)
// .csrfTokenRequestHandler(requestHandler));
// }
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
return http.build(); return http.build();
} }
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() { public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); provider.setUserDetailsService(userDetailsService);
@@ -301,6 +556,46 @@ public class SecurityConfiguration {
return provider; return provider;
} }
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
@Bean @Bean
public IPRateLimitingFilter rateLimitingFilter() { public IPRateLimitingFilter rateLimitingFilter() {
// Example limit TODO add config level // Example limit TODO add config level

View File

@@ -22,31 +22,22 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Slf4j
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
private final ApplicationProperties applicationProperties;
private final UserService userService; private final UserService userService;
private final SessionPersistentRegistry sessionPersistentRegistry; private final SessionPersistentRegistry sessionPersistentRegistry;
private final boolean loginEnabledValue; private final boolean loginEnabledValue;
public UserAuthenticationFilter( public UserAuthenticationFilter(
@Lazy ApplicationProperties applicationProperties,
@Lazy UserService userService, @Lazy UserService userService,
SessionPersistentRegistry sessionPersistentRegistry, SessionPersistentRegistry sessionPersistentRegistry,
@Qualifier("loginEnabled") boolean loginEnabledValue) { @Qualifier("loginEnabled") boolean loginEnabledValue) {
this.applicationProperties = applicationProperties;
this.userService = userService; this.userService = userService;
this.sessionPersistentRegistry = sessionPersistentRegistry; this.sessionPersistentRegistry = sessionPersistentRegistry;
this.loginEnabledValue = loginEnabledValue; this.loginEnabledValue = loginEnabledValue;
@@ -130,67 +121,33 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Check if the authenticated user is disabled and invalidate their session if so // Check if the authenticated user is disabled and invalidate their session if so
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Security securityProp = applicationProperties.getSecurity();
LoginMethod loginMethod = LoginMethod.UNKNOWN;
boolean blockRegistration = false;
// Extract username and determine the login method
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String username = null; String username = null;
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername(); username = ((UserDetails) principal).getUsername();
loginMethod = LoginMethod.USERDETAILS;
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName(); username = ((OAuth2User) principal).getName();
loginMethod = LoginMethod.OAUTH2USER;
OAUTH2 oAuth = securityProp.getOauth2();
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName(); username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
loginMethod = LoginMethod.SAML2USER;
SAML2 saml2 = securityProp.getSaml2();
blockRegistration = saml2 != null && saml2.getBlockRegistration();
} else if (principal instanceof String) { } else if (principal instanceof String) {
username = (String) principal; username = (String) principal;
loginMethod = LoginMethod.STRINGUSER;
} }
// Retrieve all active sessions for the user
List<SessionInformation> sessionsInformations = List<SessionInformation> sessionsInformations =
sessionPersistentRegistry.getAllSessions(principal, false); sessionPersistentRegistry.getAllSessions(principal, false);
// Check if the user exists, is disabled, or needs session invalidation
if (username != null) { if (username != null) {
log.debug("Validating user: {}", username);
boolean isUserExists = userService.usernameExistsIgnoreCase(username); boolean isUserExists = userService.usernameExistsIgnoreCase(username);
boolean isUserDisabled = userService.isUserDisabled(username); boolean isUserDisabled = userService.isUserDisabled(username);
boolean notSsoLogin =
!loginMethod.equals(LoginMethod.OAUTH2USER)
&& !loginMethod.equals(LoginMethod.SAML2USER);
// Block user registration if not allowed by configuration
if (blockRegistration && !isUserExists) {
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
response.sendRedirect(
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
return;
}
// Expire sessions and logout if the user does not exist or is disabled
if (!isUserExists || isUserDisabled) { if (!isUserExists || isUserDisabled) {
log.info(
"Invalidating session for disabled or non-existent user: {}", username);
for (SessionInformation sessionsInformation : sessionsInformations) { for (SessionInformation sessionsInformation : sessionsInformations) {
sessionsInformation.expireNow(); sessionsInformation.expireNow();
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId()); sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
} }
} }
// Redirect to logout if credentials are invalid if (!isUserExists) {
if (!isUserExists && notSsoLogin) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true"); response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
return; return;
} }
@@ -204,25 +161,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
private enum LoginMethod {
USERDETAILS("UserDetails"),
OAUTH2USER("OAuth2User"),
STRINGUSER("StringUser"),
UNKNOWN("Unknown"),
SAML2USER("Saml2User");
private String method;
LoginMethod(String method) {
this.method = method;
}
@Override
public String toString() {
return method;
}
}
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -21,12 +20,11 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.*; import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@@ -44,7 +42,7 @@ public class UserService implements UserServiceInterface {
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
private final DatabaseInterface databaseService; private final DatabaseBackupInterface databaseBackupHelper;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@@ -54,14 +52,14 @@ public class UserService implements UserServiceInterface {
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
MessageSource messageSource, MessageSource messageSource,
SessionPersistentRegistry sessionRegistry, SessionPersistentRegistry sessionRegistry,
DatabaseInterface databaseService, DatabaseBackupInterface databaseBackupHelper,
ApplicationProperties applicationProperties) { ApplicationProperties applicationProperties) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.authorityRepository = authorityRepository; this.authorityRepository = authorityRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.messageSource = messageSource; this.messageSource = messageSource;
this.sessionRegistry = sessionRegistry; this.sessionRegistry = sessionRegistry;
this.databaseService = databaseService; this.databaseBackupHelper = databaseBackupHelper;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
} }
@@ -78,7 +76,7 @@ public class UserService implements UserServiceInterface {
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
} }
@@ -165,12 +163,12 @@ public class UserService implements UserServiceInterface {
} }
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
saveUser(username, authenticationType, Role.USER.getRoleId()); saveUser(username, authenticationType, Role.USER.getRoleId());
} }
public void saveUser(String username, AuthenticationType authenticationType, String role) public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -181,11 +179,11 @@ public class UserService implements UserServiceInterface {
user.addAuthority(new Authority(role, user)); user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType); user.setAuthenticationType(authenticationType);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password) public void saveUser(String username, String password)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -195,11 +193,11 @@ public class UserService implements UserServiceInterface {
user.setEnabled(true); user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role, boolean firstLogin) public void saveUser(String username, String password, String role, boolean firstLogin)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -211,11 +209,11 @@ public class UserService implements UserServiceInterface {
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role) public void saveUser(String username, String password, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
saveUser(username, password, role, false); saveUser(username, password, role, false);
} }
@@ -249,7 +247,7 @@ public class UserService implements UserServiceInterface {
} }
public void updateUserSettings(String username, Map<String, String> updates) public void updateUserSettings(String username, Map<String, String> updates)
throws SQLException, UnsupportedProviderException { throws IOException {
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username); Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
@@ -261,7 +259,7 @@ public class UserService implements UserServiceInterface {
settingsMap.putAll(updates); settingsMap.putAll(updates);
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
} }
@@ -282,45 +280,38 @@ public class UserService implements UserServiceInterface {
} }
public void changeUsername(User user, String newUsername) public void changeUsername(User user, String newUsername)
throws IllegalArgumentException, throws IllegalArgumentException, IOException {
IOException,
SQLException,
UnsupportedProviderException {
if (!isUsernameValid(newUsername)) { if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
user.setUsername(newUsername); user.setUsername(newUsername);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changePassword(User user, String newPassword) public void changePassword(User user, String newPassword) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeFirstUse(User user, boolean firstUse) public void changeFirstUse(User user, boolean firstUse) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setFirstLogin(firstUse); user.setFirstLogin(firstUse);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeRole(User user, String newRole) public void changeRole(User user, String newRole) throws IOException {
throws SQLException, UnsupportedProviderException {
Authority userAuthority = this.findRole(user); Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole); userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority); authorityRepository.save(userAuthority);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeUserEnabled(User user, Boolean enbeled) public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setEnabled(enbeled); user.setEnabled(enbeled);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
@@ -406,8 +397,7 @@ public class UserService implements UserServiceInterface {
} }
@Transactional @Transactional
public void syncCustomApiUser(String customApiKey) public void syncCustomApiUser(String customApiKey) throws IOException {
throws SQLException, UnsupportedProviderException {
if (customApiKey == null || customApiKey.trim().length() == 0) { if (customApiKey == null || customApiKey.trim().length() == 0) {
return; return;
} }
@@ -424,14 +414,14 @@ public class UserService implements UserServiceInterface {
user.setApiKey(customApiKey); user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user)); user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} else { } else {
// Update API key if it has changed // Update API key if it has changed
User user = existingUser.get(); User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) { if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey); user.setApiKey(customApiKey);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
} }
} }

View File

@@ -0,0 +1,232 @@
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("|");
}
}

View File

@@ -1,139 +0,0 @@
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");
}
}
}

View File

@@ -1,301 +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.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("|");
}
}

View File

@@ -1,27 +1,21 @@
package stirling.software.SPDF.config.security.database; package stirling.software.SPDF.config.security.database;
import java.sql.SQLException; import java.io.IOException;
import org.springframework.context.annotation.Conditional;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.controller.api.H2SQLCondition;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component @Component
@Conditional(H2SQLCondition.class)
public class ScheduledTasks { public class ScheduledTasks {
private final DatabaseInterface databaseService; private final DatabaseBackupHelper databaseBackupService;
public ScheduledTasks(DatabaseInterface databaseService) { public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
this.databaseService = databaseService; this.databaseBackupService = databaseBackupService;
} }
@Scheduled(cron = "0 0 0 * * ?") @Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws SQLException, UnsupportedProviderException { public void performBackup() throws IOException {
databaseService.exportDatabase(); databaseBackupService.exportDatabase();
} }
} }

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.config.security.oauth2; package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -19,7 +18,6 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler public class CustomOAuth2AuthenticationSuccessHandler
@@ -99,8 +97,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
} }
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
} }
} }
} }

View File

@@ -1,213 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.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;
};
}
}

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.config.security.saml2; package stirling.software.SPDF.config.security.saml2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -19,7 +18,6 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@@ -111,7 +109,7 @@ public class CustomSaml2AuthenticationSuccessHandler
log.debug("Successfully processed authentication for user: {}", username); log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException e) {
log.debug( log.debug(
"Invalid username detected for user: {}, redirecting to logout", "Invalid username detected for user: {}, redirecting to logout",
username); username);

View File

@@ -11,11 +11,13 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Component
@Slf4j @Slf4j
public class CustomSaml2ResponseAuthenticationConverter public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> { implements Converter<ResponseToken, Saml2Authentication> {

View File

@@ -1,136 +0,0 @@
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;
}
}

View File

@@ -8,7 +8,6 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -25,20 +24,19 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.database.DatabaseService; import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Slf4j @Slf4j
@Controller @Controller
@RequestMapping("/api/v1/database") @RequestMapping("/api/v1/database")
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@Conditional(H2SQLCondition.class)
@Tag(name = "Database", description = "Database APIs for backup, import, and management") @Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController { public class DatabaseController {
private final DatabaseService databaseService; private final DatabaseBackupHelper databaseBackupHelper;
public DatabaseController(DatabaseService databaseService) { public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseService = databaseService; this.databaseBackupHelper = databaseBackupHelper;
} }
@Operation( @Operation(
@@ -59,7 +57,7 @@ public class DatabaseController {
Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) { try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
boolean importSuccess = databaseService.importDatabaseFromUI(tempTemplatePath); boolean importSuccess = databaseBackupHelper.importDatabaseFromUI(tempTemplatePath);
if (importSuccess) { if (importSuccess) {
redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed"); redirectAttributes.addAttribute("infoMessage", "importIntoDatabaseSuccessed");
} else { } else {
@@ -79,20 +77,21 @@ public class DatabaseController {
@GetMapping("/import-database-file/{fileName}") @GetMapping("/import-database-file/{fileName}")
public String importDatabaseFromBackupUI( public String importDatabaseFromBackupUI(
@Parameter(description = "Name of the file to import", required = true) @PathVariable @Parameter(description = "Name of the file to import", required = true) @PathVariable
String fileName) { String fileName)
throws IOException {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty"; return "redirect:/database?error=fileNullOrEmpty";
} }
// Check if the file exists in the backup list // Check if the file exists in the backup list
boolean fileExists = boolean fileExists =
databaseService.getBackupList().stream() databaseBackupHelper.getBackupList().stream()
.anyMatch(backup -> backup.getFileName().equals(fileName)); .anyMatch(backup -> backup.getFileName().equals(fileName));
if (!fileExists) { if (!fileExists) {
log.error("File {} not found in backup list", fileName); log.error("File {} not found in backup list", fileName);
return "redirect:/database?error=fileNotFound"; return "redirect:/database?error=fileNotFound";
} }
log.info("Received file: {}", fileName); log.info("Received file: {}", fileName);
if (databaseService.importDatabaseFromUI(fileName)) { if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName); log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
} }
@@ -111,7 +110,7 @@ public class DatabaseController {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
try { try {
if (databaseService.deleteBackupFile(fileName)) { if (databaseBackupHelper.deleteBackupFile(fileName)) {
log.info("Deleted file: {}", fileName); log.info("Deleted file: {}", fileName);
} else { } else {
log.error("Failed to delete file: {}", fileName); log.error("Failed to delete file: {}", fileName);
@@ -136,7 +135,7 @@ public class DatabaseController {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
try { try {
Path filePath = databaseService.getBackupFilePath(fileName); Path filePath = databaseBackupHelper.getBackupFilePath(fileName);
InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath)); InputStreamResource resource = new InputStreamResource(Files.newInputStream(filePath));
return ResponseEntity.ok() return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + fileName)
@@ -158,9 +157,14 @@ public class DatabaseController {
+ " database management page.") + " database management page.")
@GetMapping("/createDatabaseBackup") @GetMapping("/createDatabaseBackup")
public String createDatabaseBackup() { public String createDatabaseBackup() {
try {
log.info("Starting database backup creation..."); log.info("Starting database backup creation...");
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
log.info("Database backup successfully created."); log.info("Database backup successfully created.");
} catch (IOException e) {
log.error("Error creating database backup: {}", e.getMessage(), e);
return "redirect:/database?error=" + e.getMessage();
}
return "redirect:/database?infoMessage=backupCreated"; return "redirect:/database?infoMessage=backupCreated";
} }
} }

View File

@@ -1,19 +0,0 @@
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));
}
}

View File

@@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@@ -34,8 +33,7 @@ public class SettingsController {
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) { if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED) return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body( .body(
"Setting has already been set, To adjust please edit " "Setting has already been set, To adjust please edit /config/settings.yml");
+ InstallationPathConfig.getSettingsPath());
} }
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false); GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled)); applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.sql.SQLException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -34,7 +33,6 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.api.user.UsernameAndPass; import stirling.software.SPDF.model.api.user.UsernameAndPass;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Controller @Controller
@Tag(name = "User", description = "User APIs") @Tag(name = "User", description = "User APIs")
@@ -54,7 +52,7 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
throws SQLException, UnsupportedProviderException { throws IOException {
if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) { if (userService.usernameExistsIgnoreCase(requestModel.getUsername())) {
model.addAttribute("error", "Username already exists"); model.addAttribute("error", "Username already exists");
return "register"; return "register";
@@ -76,7 +74,7 @@ public class UserController {
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws IOException, SQLException, UnsupportedProviderException { throws IOException {
if (!userService.isUsernameValid(newUsername)) { if (!userService.isUsernameValid(newUsername)) {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
} }
@@ -119,7 +117,7 @@ public class UserController {
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws SQLException, UnsupportedProviderException { throws IOException {
if (principal == null) { if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated", true); return new RedirectView("/change-creds?messageType=notAuthenticated", true);
} }
@@ -147,7 +145,7 @@ public class UserController {
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws SQLException, UnsupportedProviderException { throws IOException {
if (principal == null) { if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
@@ -168,7 +166,7 @@ public class UserController {
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) public String updateUserSettings(HttpServletRequest request, Principal principal)
throws SQLException, UnsupportedProviderException { throws IOException {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
@@ -190,7 +188,7 @@ public class UserController {
@RequestParam(name = "authType") String authType, @RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!userService.isUsernameValid(username)) { if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true); return new RedirectView("/addUsers?messageType=invalidUsername", true);
} }
@@ -234,7 +232,7 @@ public class UserController {
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
Authentication authentication) Authentication authentication)
throws SQLException, UnsupportedProviderException { throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
@@ -272,7 +270,7 @@ public class UserController {
@PathVariable("username") String username, @PathVariable("username") String username,
@RequestParam("enabled") boolean enabled, @RequestParam("enabled") boolean enabled,
Authentication authentication) Authentication authentication)
throws SQLException, UnsupportedProviderException { throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);

View File

@@ -13,9 +13,6 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.ImageType; import org.apache.pdfbox.rendering.ImageType;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -34,8 +31,11 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.*; import stirling.software.SPDF.utils.CheckProgramInstall;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@@ -62,20 +62,14 @@ public class ConvertImgPDFController {
String singleOrMultiple = request.getSingleOrMultiple(); String singleOrMultiple = request.getSingleOrMultiple();
String colorType = request.getColorType(); String colorType = request.getColorType();
String dpi = request.getDpi(); String dpi = request.getDpi();
String pageNumbers = request.getPageNumbers();
Path tempFile = null; Path tempFile = null;
Path tempOutputDir = null; Path tempOutputDir = null;
Path tempPdfPath = null; Path tempPdfPath = null;
byte[] result = null; byte[] result = null;
String[] pageOrderArr =
(pageNumbers != null && !pageNumbers.trim().isEmpty())
? pageNumbers.split(",")
: new String[] {"all"};
;
try {
// Load the input PDF
byte[] newPdfBytes = rearrangePdfPages(file.getBytes(), pageOrderArr);
try {
byte[] pdfBytes = file.getBytes();
ImageType colorTypeResult = ImageType.RGB; ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) { if ("greyscale".equals(colorType)) {
colorTypeResult = ImageType.GRAY; colorTypeResult = ImageType.GRAY;
@@ -90,7 +84,7 @@ public class ConvertImgPDFController {
result = result =
PdfUtils.convertFromPdf( PdfUtils.convertFromPdf(
newPdfBytes, pdfBytes,
"webp".equalsIgnoreCase(imageFormat) "webp".equalsIgnoreCase(imageFormat)
? "png" ? "png"
: imageFormat.toUpperCase(), : imageFormat.toUpperCase(),
@@ -233,46 +227,4 @@ public class ConvertImgPDFController {
String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat); String mimeType = URLConnection.guessContentTypeFromName("." + imageFormat);
return "null".equals(mimeType) ? "application/octet-stream" : mimeType; return "null".equals(mimeType) ? "application/octet-stream" : mimeType;
} }
/**
* Rearranges the pages of the given PDF document based on the specified page order.
*
* @param pdfBytes The byte array of the original PDF file.
* @param pageOrderArr An array of page numbers indicating the new order.
* @return A byte array of the rearranged PDF.
* @throws IOException If an error occurs while processing the PDF.
*/
private byte[] rearrangePdfPages(byte[] pdfBytes, String[] pageOrderArr) throws IOException {
// Load the input PDF
PDDocument document = Loader.loadPDF(pdfBytes);
int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
// Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>();
for (int pageIndex : newPageOrder) {
newPages.add(document.getPage(pageIndex));
}
// Remove all the pages from the original document
for (int i = document.getNumberOfPages() - 1; i >= 0; i--) {
document.removePage(i);
}
// Add the pages in the new order
for (PDPage page : newPages) {
document.addPage(page);
}
// Convert PDDocument to byte array
byte[] newPdfBytes;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
document.save(baos);
newPdfBytes = baos.toByteArray();
} finally {
document.close();
}
return newPdfBytes;
}
} }

View File

@@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint; import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@@ -44,7 +44,7 @@ public class ApiDocService {
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPDFApplication.getStaticPort(); String port = SPdfApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }

View File

@@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.model.api.HandleDataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@@ -34,12 +35,22 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Pipeline", description = "Pipeline APIs") @Tag(name = "Pipeline", description = "Pipeline APIs")
public class PipelineController { public class PipelineController {
final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/";
private final PipelineProcessor processor; private final PipelineProcessor processor;
private final ApplicationProperties applicationProperties;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public PipelineController(PipelineProcessor processor, ObjectMapper objectMapper) { public PipelineController(
PipelineProcessor processor,
ApplicationProperties applicationProperties,
ObjectMapper objectMapper) {
this.processor = processor; this.processor = processor;
this.applicationProperties = applicationProperties;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }

View File

@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@@ -24,7 +25,6 @@ import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.FileMonitor; import stirling.software.SPDF.utils.FileMonitor;
@@ -48,12 +48,14 @@ public class PipelineDirectoryProcessor {
public PipelineDirectoryProcessor( public PipelineDirectoryProcessor(
ObjectMapper objectMapper, ObjectMapper objectMapper,
ApiDocService apiDocService, ApiDocService apiDocService,
@Qualifier("watchedFoldersDir") String watchedFoldersDir,
@Qualifier("finishedFoldersDir") String finishedFoldersDir,
PipelineProcessor processor, PipelineProcessor processor,
FileMonitor fileMonitor) { FileMonitor fileMonitor) {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.apiDocService = apiDocService; this.apiDocService = apiDocService;
this.watchedFoldersDir = InstallationPathConfig.getPipelineWatchedFoldersDir(); this.watchedFoldersDir = watchedFoldersDir;
this.finishedFoldersDir = InstallationPathConfig.getPipelineFinishedFoldersDir(); this.finishedFoldersDir = finishedFoldersDir;
this.processor = processor; this.processor = processor;
this.fileMonitor = fileMonitor; this.fileMonitor = fileMonitor;
} }

View File

@@ -30,7 +30,7 @@ import io.github.pixee.security.ZipSecurity;
import jakarta.servlet.ServletContext; import jakarta.servlet.ServletContext;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
@@ -80,7 +80,7 @@ public class PipelineProcessor {
private String getBaseUrl() { private String getBaseUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPDFApplication.getStaticPort(); String port = SPdfApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/"; return "http://localhost:" + port + contextPath + "/";
} }

View File

@@ -3,18 +3,13 @@ package stirling.software.SPDF.controller.api.security;
import java.awt.*; import java.awt.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,15 +22,11 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.model.api.security.ManualRedactPdfRequest;
import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.model.api.security.RedactPdfRequest;
import stirling.software.SPDF.model.api.security.RedactionArea;
import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import stirling.software.SPDF.utils.propertyeditor.StringToArrayListPropertyEditor;
@RestController @RestController
@RequestMapping("/api/v1/security") @RequestMapping("/api/v1/security")
@@ -50,120 +41,6 @@ public class RedactController {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(
List.class, "redactions", new StringToArrayListPropertyEditor());
}
@PostMapping(value = "/redact", consumes = "multipart/form-data")
@Operation(
summary = "Redacts areas and pages in a PDF document",
description =
"This operation takes an input PDF file with a list of areas, page number(s)/range(s)/function(s) to redact. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPDF(@ModelAttribute ManualRedactPdfRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
List<RedactionArea> redactionAreas = request.getRedactions();
PDDocument document = pdfDocumentFactory.load(file);
PDPageTree allPages = document.getDocumentCatalog().getPages();
redactPages(request, document, allPages);
redactAreas(redactionAreas, document, allPages);
if (request.isConvertPDFToImage()) {
PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
document.close();
document = convertedPdf;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
+ "_redacted.pdf");
}
private void redactAreas(
List<RedactionArea> redactionAreas, PDDocument document, PDPageTree allPages)
throws IOException {
Color redactColor = null;
for (RedactionArea redactionArea : redactionAreas) {
if (redactionArea.getPage() == null
|| redactionArea.getPage() <= 0
|| redactionArea.getHeight() == null
|| redactionArea.getHeight() <= 0.0D
|| redactionArea.getWidth() == null
|| redactionArea.getWidth() <= 0.0D) continue;
PDPage page = allPages.get(redactionArea.getPage() - 1);
PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
redactColor = decodeOrDefault(redactionArea.getColor(), Color.BLACK);
contentStream.setNonStrokingColor(redactColor);
float x = redactionArea.getX().floatValue();
float y = redactionArea.getY().floatValue();
float width = redactionArea.getWidth().floatValue();
float height = redactionArea.getHeight().floatValue();
PDRectangle box = page.getBBox();
contentStream.addRect(x, box.getHeight() - y - height, width, height);
contentStream.fill();
contentStream.close();
}
}
private void redactPages(
ManualRedactPdfRequest request, PDDocument document, PDPageTree allPages)
throws IOException {
Color redactColor = decodeOrDefault(request.getPageRedactionColor(), Color.BLACK);
List<Integer> pageNumbers = getPageNumbers(request, allPages.getCount());
for (Integer pageNumber : pageNumbers) {
PDPage page = allPages.get(pageNumber);
PDPageContentStream contentStream =
new PDPageContentStream(
document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(redactColor);
PDRectangle box = page.getBBox();
contentStream.addRect(0, 0, box.getWidth(), box.getHeight());
contentStream.fill();
contentStream.close();
}
}
private Color decodeOrDefault(String hex, Color defaultColor) {
Color color = null;
try {
color = Color.decode(hex);
} catch (Exception e) {
color = defaultColor;
}
return color;
}
private List<Integer> getPageNumbers(ManualRedactPdfRequest request, int pagesCount) {
String pageNumbersInput = request.getPageNumbers();
String[] parsedPageNumbers =
pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0];
List<Integer> pageNumbers =
GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
Collections.sort(pageNumbers);
return pageNumbers;
}
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data") @PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Redacts listOfText in a PDF document", summary = "Redacts listOfText in a PDF document",

View File

@@ -14,11 +14,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.*;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.util.Store; import org.bouncycastle.util.Store;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -163,9 +163,6 @@ public class AccountWebController {
case "invalid_destination": case "invalid_destination":
erroroauth = "login.invalid_destination"; erroroauth = "login.invalid_destination";
break; break;
case "relying_party_registration_not_found":
erroroauth = "login.relyingPartyRegistrationNotFound";
break;
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to": case "invalid_in_response_to":

View File

@@ -11,17 +11,17 @@ import org.springframework.web.bind.annotation.GetMapping;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.config.security.database.DatabaseService; import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
import stirling.software.SPDF.utils.FileInfo; import stirling.software.SPDF.utils.FileInfo;
@Controller @Controller
@Tag(name = "Database Management", description = "Database management and security APIs") @Tag(name = "Database Management", description = "Database management and security APIs")
public class DatabaseWebController { public class DatabaseWebController {
private final DatabaseService databaseService; private final DatabaseBackupHelper databaseBackupHelper;
public DatabaseWebController(DatabaseService databaseService) { public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseService = databaseService; this.databaseBackupHelper = databaseBackupHelper;
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -34,12 +34,9 @@ public class DatabaseWebController {
} else if (confirmed != null) { } else if (confirmed != null) {
model.addAttribute("infoMessage", confirmed); model.addAttribute("infoMessage", confirmed);
} }
List<FileInfo> backupList = databaseService.getBackupList(); List<FileInfo> backupList = databaseBackupHelper.getBackupList();
model.addAttribute("backupFiles", backupList); model.addAttribute("backupFiles", backupList);
model.addAttribute("databaseVersion", databaseService.getH2Version()); model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
if ("Unknown".equalsIgnoreCase(databaseService.getH2Version())) {
model.addAttribute("infoMessage", "notSupported");
}
return "database"; return "database";
} }
} }

View File

@@ -25,7 +25,6 @@ import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.SignatureFile; import stirling.software.SPDF.model.SignatureFile;
import stirling.software.SPDF.service.SignatureService; import stirling.software.SPDF.service.SignatureService;
@@ -35,6 +34,8 @@ import stirling.software.SPDF.service.SignatureService;
@Slf4j @Slf4j
public class GeneralWebController { public class GeneralWebController {
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
private final SignatureService signatureService; private final SignatureService signatureService;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final ResourceLoader resourceLoader; private final ResourceLoader resourceLoader;
@@ -222,9 +223,7 @@ public class GeneralWebController {
// Extract font names from classpath // Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2")); fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory // Extract font names from external directory
fontNames.addAll( fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
getFontNamesFromLocation(
"file:" + InstallationPathConfig.getStaticPath() + "fonts/*"));
return fontNames; return fontNames;
} }

View File

@@ -18,12 +18,6 @@ public class SecurityWebController {
return "security/auto-redact"; return "security/auto-redact";
} }
@GetMapping("/redact")
public String redactForm(Model model) {
model.addAttribute("currentPage", "redact");
return "security/redact";
}
@GetMapping("/add-password") @GetMapping("/add-password")
@Hidden @Hidden
public String addPasswordForm(Model model) { public String addPasswordForm(Model model) {

View File

@@ -1,7 +1,5 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
@@ -15,23 +13,18 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.config.YamlPropertySourceFactory; import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
@@ -40,37 +33,11 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Configuration @Configuration
@ConfigurationProperties(prefix = "") @ConfigurationProperties(prefix = "")
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
@Data @Data
@Order(Ordered.HIGHEST_PRECEDENCE) @Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class ApplicationProperties { public class ApplicationProperties {
@Bean
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
throws IOException {
String configPath = InstallationPathConfig.getSettingsPath();
log.debug("Attempting to load settings from: " + configPath);
File file = new File(configPath);
if (!file.exists()) {
log.error("Warning: Settings file does not exist at: " + configPath);
}
Resource resource = new FileSystemResource(configPath);
if (!resource.exists()) {
throw new FileNotFoundException("Settings file not found at: " + configPath);
}
EncodedResource encodedResource = new EncodedResource(resource);
PropertySource<?> propertySource =
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
environment.getPropertySources().addFirst(propertySource);
log.debug("Loaded properties: " + propertySource.getSource());
return propertySource;
}
private Legal legal = new Legal(); private Legal legal = new Legal();
private Security security = new Security(); private Security security = new Security();
private System system = new System(); private System system = new System();
@@ -112,6 +79,23 @@ public class ApplicationProperties {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
} }
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public enum LoginMethods { public enum LoginMethods {
ALL("all"), ALL("all"),
NORMAL("normal"), NORMAL("normal"),
@@ -130,23 +114,6 @@ public class ApplicationProperties {
} }
} }
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
@Data @Data
public static class InitialLogin { public static class InitialLogin {
private String username; private String username;
@@ -186,7 +153,6 @@ public class ApplicationProperties {
} }
public Resource getSpCert() { public Resource getSpCert() {
if (spCert == null) return null;
if (spCert.startsWith("classpath:")) { if (spCert.startsWith("classpath:")) {
return new ClassPathResource(spCert.substring("classpath:".length())); return new ClassPathResource(spCert.substring("classpath:".length()));
} else { } else {
@@ -195,7 +161,6 @@ public class ApplicationProperties {
} }
public Resource getidpCert() { public Resource getidpCert() {
if (idpCert == null) return null;
if (idpCert.startsWith("classpath:")) { if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length())); return new ClassPathResource(idpCert.substring("classpath:".length()));
} else { } else {
@@ -282,42 +247,6 @@ public class ApplicationProperties {
private String tessdataDir; private String tessdataDir;
private Boolean enableAlphaFunctionality; private Boolean enableAlphaFunctionality;
private String enableAnalytics; private String enableAnalytics;
private Datasource datasource;
}
@Data
public static class Datasource {
private boolean enableCustomDatabase;
private String customDatabaseUrl;
private String type;
private String hostName;
private Integer port;
private String name;
private String username;
@ToString.Exclude private String password;
}
public enum Driver {
H2("h2"),
POSTGRESQL("postgresql"),
ORACLE("oracle"),
MYSQL("mysql");
private final String driverName;
Driver(String driverName) {
this.driverName = driverName;
}
@Override
public String toString() {
return """
Driver {
driverName='%s'
}
"""
.formatted(driverName);
}
} }
@Data @Data
@@ -366,7 +295,6 @@ public class ApplicationProperties {
private boolean enabled; private boolean enabled;
@ToString.Exclude private String key; @ToString.Exclude private String key;
private int maxUsers; private int maxUsers;
private boolean ssoAutoLogin;
private CustomMetadata customMetadata = new CustomMetadata(); private CustomMetadata customMetadata = new CustomMetadata();
@Data @Data

View File

@@ -5,6 +5,7 @@ import java.util.Date;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.Data; import lombok.Data;
@@ -14,7 +15,7 @@ import lombok.Data;
public class SessionEntity implements Serializable { public class SessionEntity implements Serializable {
@Id private String sessionId; @Id private String sessionId;
private String principalName; @Lob private String principalName;
private Date lastRequest; private Date lastRequest;

View File

@@ -47,7 +47,7 @@ public class User implements Serializable {
@ElementCollection @ElementCollection
@MapKeyColumn(name = "setting_key") @MapKeyColumn(name = "setting_key")
@Lob @Lob
@Column(name = "setting_value", columnDefinition = "text") @Column(name = "setting_value", columnDefinition = "CLOB")
@CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id"))
private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings. private Map<String, String> settings = new HashMap<>(); // Key-value pairs of settings.

View File

@@ -21,11 +21,6 @@ public class ConvertToImageRequest extends PDFFile {
allowableValues = {"single", "multiple"}) allowableValues = {"single", "multiple"})
private String singleOrMultiple; private String singleOrMultiple;
@Schema(
description =
"The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"")
private String pageNumbers;
@Schema( @Schema(
description = "The color type of the output image(s)", description = "The color type of the output image(s)",
allowableValues = {"color", "greyscale", "blackwhite"}) allowableValues = {"color", "greyscale", "blackwhite"})

View File

@@ -1,22 +0,0 @@
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;
}

View File

@@ -1,26 +0,0 @@
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;
}

View File

@@ -1,7 +0,0 @@
package stirling.software.SPDF.model.exception;
public class BackupNotFoundException extends RuntimeException {
public BackupNotFoundException(String message) {
super(message);
}
}

View File

@@ -13,19 +13,14 @@ import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils; import org.thymeleaf.util.StringUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.SignatureFile; import stirling.software.SPDF.model.SignatureFile;
@Service @Service
@Slf4j @Slf4j
public class SignatureService { public class SignatureService {
private final String SIGNATURE_BASE_PATH; private static final String SIGNATURE_BASE_PATH = "customFiles/signatures/";
private final String ALL_USERS_FOLDER = "ALL_USERS"; private static final String ALL_USERS_FOLDER = "ALL_USERS";
public SignatureService() {
SIGNATURE_BASE_PATH = InstallationPathConfig.getSignaturesPath();
}
public boolean hasAccessToFile(String username, String fileName) throws IOException { public boolean hasAccessToFile(String username, String fileName) throws IOException {
validateFileName(fileName); validateFileName(fileName);

View File

@@ -15,7 +15,6 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
@Component @Component
@Slf4j @Slf4j
@@ -35,7 +34,9 @@ public class FileMonitor {
* monitored, false otherwise * monitored, false otherwise
*/ */
@Autowired @Autowired
public FileMonitor(@Qualifier("directoryFilter") Predicate<Path> pathFilter) public FileMonitor(
@Qualifier("watchedFoldersDir") String rootDirectory,
@Qualifier("directoryFilter") Predicate<Path> pathFilter)
throws IOException { throws IOException {
this.newlyDiscoveredFiles = new HashSet<>(); this.newlyDiscoveredFiles = new HashSet<>();
this.path2KeyMapping = new HashMap<>(); this.path2KeyMapping = new HashMap<>();
@@ -43,7 +44,7 @@ public class FileMonitor {
this.pathFilter = pathFilter; this.pathFilter = pathFilter;
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet(); this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
this.watchService = FileSystems.getDefault().newWatchService(); this.watchService = FileSystems.getDefault().newWatchService();
this.rootDir = Path.of(InstallationPathConfig.getPipelineWatchedFoldersDir()); this.rootDir = Path.of(rootDirectory);
} }
private boolean shouldNotProcess(Path path) { private boolean shouldNotProcess(Path path) {

View File

@@ -26,7 +26,6 @@ import io.github.pixee.security.HostValidator;
import io.github.pixee.security.Urls; import io.github.pixee.security.Urls;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig;
@Slf4j @Slf4j
public class GeneralUtils { public class GeneralUtils {
@@ -84,7 +83,7 @@ public class GeneralUtils {
// Allow only http and https protocols // Allow only http and https protocols
String protocol = url.getProtocol(); String protocol = url.getProtocol();
if (!"http".equals(protocol) && !"https".equals(protocol)) { if (!protocol.equals("http") && !protocol.equals("https")) {
return false; // Disallow other protocols return false; // Disallow other protocols
} }
@@ -221,54 +220,32 @@ public class GeneralUtils {
throw new IllegalArgumentException("Invalid expression"); throw new IllegalArgumentException("Invalid expression");
} }
for (int n = 1; n <= maxValue; n++) { int n = 0;
while (true) {
// Replace 'n' with the current value of n, correctly handling numbers before // Replace 'n' with the current value of n, correctly handling numbers before
// 'n' // 'n'
String sanitizedExpression = sanitizeNFunction(expression, n); String sanitizedExpression = insertMultiplicationBeforeN(expression, n);
Double result = evaluator.evaluate(sanitizedExpression); Double result = evaluator.evaluate(sanitizedExpression);
// Check if the result is null or not within bounds // Check if the result is null or not within bounds
if (result == null) break; if (result == null || result <= 0 || result.intValue() > maxValue) {
if (n != 0) break;
if (result.intValue() > 0 && result.intValue() <= maxValue) } else {
results.add(result.intValue()); results.add(result.intValue());
} }
n++;
}
return results; return results;
} }
private static String sanitizeNFunction(String expression, int nValue) {
String sanitizedExpression = expression.replace(" ", "");
String multiplyByOpeningRoundBracketPattern =
"([0-9n)])\\("; // example: n(n-1), 9(n-1), (n-1)(n-2)
sanitizedExpression =
sanitizedExpression.replaceAll(multiplyByOpeningRoundBracketPattern, "$1*(");
String multiplyByClosingRoundBracketPattern =
"\\)([0-9n)])"; // example: (n-1)n, (n-1)9, (n-1)(n-2)
sanitizedExpression =
sanitizedExpression.replaceAll(multiplyByClosingRoundBracketPattern, ")*$1");
sanitizedExpression = insertMultiplicationBeforeN(sanitizedExpression, nValue);
return sanitizedExpression;
}
private static String insertMultiplicationBeforeN(String expression, int nValue) { private static String insertMultiplicationBeforeN(String expression, int nValue) {
// Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n") // Insert multiplication between a number and 'n' (e.g., "4n" becomes "4*n")
String withMultiplication = expression.replaceAll("(\\d)n", "$1*n"); String withMultiplication = expression.replaceAll("(\\d)n", "$1*n");
withMultiplication = formatConsecutiveNsForNFunction(withMultiplication);
// Now replace 'n' with its current value // Now replace 'n' with its current value
return withMultiplication.replace("n", String.valueOf(nValue)); return withMultiplication.replace("n", String.valueOf(nValue));
} }
private static String formatConsecutiveNsForNFunction(String expression) {
String text = expression;
while (text.matches(".*n{2,}.*")) {
text = text.replaceAll("(?<!n)n{2}", "n*n");
}
return text;
}
private static List<Integer> handlePart(String part, int totalPages, int offset) { private static List<Integer> handlePart(String part, int totalPages, int offset) {
List<Integer> partResult = new ArrayList<>(); List<Integer> partResult = new ArrayList<>();
@@ -343,10 +320,7 @@ public class GeneralUtils {
public static void saveKeyToConfig(String id, String key, boolean autoGenerated) public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException { throws IOException {
Path path = Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
Paths.get(
InstallationPathConfig
.getSettingsPath()); // Target the configs/settings.yml
final YamlFile settingsYml = new YamlFile(path.toFile()); final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml = DumperOptions yamlOptionssettingsYml =
@@ -364,7 +338,7 @@ public class GeneralUtils {
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated) public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
throws IOException { throws IOException {
Path path = Paths.get(InstallationPathConfig.getSettingsPath()); Path path = Paths.get("configs", "settings.yml");
final YamlFile settingsYml = new YamlFile(path.toFile()); final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml = DumperOptions yamlOptionssettingsYml =

View File

@@ -0,0 +1,3 @@
package stirling.software.SPDF.utils;
public class PDFManipulationUtils {}

View File

@@ -9,7 +9,6 @@ import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -65,7 +64,7 @@ public class PDFToFile {
.runCommandWithOutputHandling(command, tempOutputDir.toFile()); .runCommandWithOutputHandling(command, tempOutputDir.toFile());
// Get output files // Get output files
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles()); List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
// Return output files in a ZIP archive // Return output files in a ZIP archive
fileName = pdfBaseName + "ToHtml.zip"; fileName = pdfBaseName + "ToHtml.zip";

View File

@@ -22,11 +22,9 @@ import org.apache.pdfbox.text.TextPosition;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.HighContrastColorCombination; import stirling.software.SPDF.model.api.misc.HighContrastColorCombination;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert; import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
@Slf4j
public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy { public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
private String textColor; private String textColor;
@@ -95,17 +93,17 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
try { try {
font = PDFontFactory.createFont(text.getFont().getCOSObject()); font = PDFontFactory.createFont(text.getFont().getCOSObject());
} catch (IOException io) { } catch (IOException io) {
log.info("Primary font not found, using fallback font."); System.out.println("Primary font not found, using fallback font.");
font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
} }
// if a character is not supported by font, then look for supported font // if a character is not supported by font, then look for supported font
try { try {
byte[] bytes = font.encode(unicodeText); byte[] bytes = font.encode(unicodeText);
} catch (IOException io) { } catch (IOException io) {
log.info("text could not be encoded "); System.out.println("text could not be encoded ");
font = checkSupportedFontForCharacter(unicodeText); font = checkSupportedFontForCharacter(unicodeText);
} catch (IllegalArgumentException ie) { } catch (IllegalArgumentException ie) {
log.info("text not supported by font "); System.out.println("text not supported by font ");
font = checkSupportedFontForCharacter(unicodeText); font = checkSupportedFontForCharacter(unicodeText);
} finally { } finally {
// if any other font is not supported, then replace default character * // if any other font is not supported, then replace default character *
@@ -159,9 +157,9 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
byte[] bytes = currentFont.encode(unicodeText); byte[] bytes = currentFont.encode(unicodeText);
return currentFont; return currentFont;
} catch (IOException io) { } catch (IOException io) {
log.info("text could not be encoded "); System.out.println("text could not be encoded ");
} catch (IllegalArgumentException ie) { } catch (IllegalArgumentException ie) {
log.info("text not supported by font "); System.out.println("text not supported by font ");
} }
} }
return null; return null;

View File

@@ -1,37 +0,0 @@
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");
}
}
}

View File

@@ -27,8 +27,9 @@ spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL #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.driver-class-name=org.h2.Driver spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=

View File

@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration> <configuration>
<define name="LOG_PATH" class="stirling.software.SPDF.config.LogbackPropertyLoader" />
<!-- Console Appender --> <!-- Console Appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
@@ -9,30 +7,35 @@
</encoder> </encoder>
</appender> </appender>
<!-- Rolling File Appender for Auth Logs --> <!-- Rolling File Appender -->
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/invalid-auths.log</file> <file>logs/invalid-auths.log</file>
<encoder> <encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern> <pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder> </encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/auth-%d{yyyy-MM-dd}.log</fileNamePattern> <!-- daily rollover and keep 7 days' worth of history -->
<fileNamePattern>logs/auth-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>1</maxHistory> <maxHistory>1</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<!-- Rolling File Appender for General Logs --> <!-- Rolling File Appender -->
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/info.log</file> <file>logs/info.log</file>
<encoder> <encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern> <pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder> </encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/info-%d{yyyy-MM-dd}.log</fileNamePattern> <!-- daily rollover and keep 7 days' worth of history -->
<fileNamePattern>logs/info-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>1</maxHistory> <maxHistory>1</maxHistory>
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<!-- Root Logger --> <!-- Root Logger -->
<root level="INFO"> <root level="INFO">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
@@ -40,9 +43,10 @@
</root> </root>
<!-- Specific Logger --> <!-- Specific Logger -->
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" <logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" level="ERROR"
level="ERROR" additivity="false"> additivity="false">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
<appender-ref ref="AUTHLOG"/> <appender-ref ref="AUTHLOG"/>
</logger> </logger>
</configuration> </configuration>

View File

@@ -82,7 +82,6 @@ pages=صفحات
loading=جارٍ التحميل... loading=جارٍ التحميل...
addToDoc=إضافة إلى المستند addToDoc=إضافة إلى المستند
reset=إعداة ضبط reset=إعداة ضبط
apply=Apply
legal.privacy=سياسة الخصوصية legal.privacy=سياسة الخصوصية
legal.terms=شروط الاستخدام legal.terms=شروط الاستخدام
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=لم يتم العثور على الملف database.fileNotFound=لم يتم العثور على الملف
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
database.failedImportFile=فشل استيراد الملف database.failedImportFile=فشل استيراد الملف
database.notSupported=This function is not available for your database connection.
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
session.refreshPage=تحديث الصفحة session.refreshPage=تحديث الصفحة
@@ -476,10 +474,6 @@ home.autoRedact.title=حجب تلقائي
home.autoRedact.desc=يحجب (يسود) النص في PDF تلقائيًا بناءً على النص المدخل home.autoRedact.desc=يحجب (يسود) النص في PDF تلقائيًا بناءً على النص المدخل
autoRedact.tags=حجب,إخفاء,تسويد,أسود,علامة,مخفي autoRedact.tags=حجب,إخفاء,تسويد,أسود,علامة,مخفي
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF إلى CSV home.tableExtraxt.title=PDF إلى CSV
home.tableExtraxt.desc=يستخرج الجداول من PDF ويحولها إلى CSV home.tableExtraxt.desc=يستخرج الجداول من PDF ويحولها إلى CSV
tableExtraxt.tags=CSV,استخراج الجدول,استخراج,تحويل tableExtraxt.tags=CSV,استخراج الجدول,استخراج,تحويل
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=طلب غير صالح
login.oauth2AccessDenied=تم رفض الوصول login.oauth2AccessDenied=تم رفض الوصول
login.oauth2InvalidTokenResponse=استجابة الرمز المميز غير صالحة login.oauth2InvalidTokenResponse=استجابة الرمز المميز غير صالحة
login.oauth2InvalidIdToken=رمز الهوية غير صالح login.oauth2InvalidIdToken=رمز الهوية غير صالح
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل الدخول حاليًا باستخدام اسم المستخدم هذا. يرجى الاتصال بالمسؤول. login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل الدخول حاليًا باستخدام اسم المستخدم هذا. يرجى الاتصال بالمسؤول.
login.alreadyLoggedIn=لقد تسجل دخولًا إلى login.alreadyLoggedIn=لقد تسجل دخولًا إلى
login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى. login.alreadyLoggedIn2=أجهزة أخرى. يرجى تسجيل الخروج من الأجهزة وحاول مرة أخرى.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=حشو إضافي مخصص
autoRedact.convertPDFToImageLabel=تحويل PDF إلى صورة PDF (يستخدم لإزالة النص خلف المربع) autoRedact.convertPDFToImageLabel=تحويل PDF إلى صورة PDF (يستخدم لإزالة النص خلف المربع)
autoRedact.submitButton=إرسال autoRedact.submitButton=إرسال
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=إظهار جافا سكريبت showJS.title=إظهار جافا سكريبت
@@ -862,8 +830,6 @@ sign.first=First page
sign.last=Last page sign.last=Last page
sign.next=Next page sign.next=Next page
sign.previous=Previous page sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=إصلاح repair.title=إصلاح
@@ -1075,7 +1041,6 @@ pdfToImage.grey=تدرج الرمادي
pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!) pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!)
pdfToImage.submit=تحويل pdfToImage.submit=تحويل
pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP. pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP.
pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=تقطيع ملف PDF
fileChooser.click=انقر هنا fileChooser.click=انقر هنا
fileChooser.or=أو fileChooser.or=أو
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
#release notes #release notes

View File

@@ -82,7 +82,6 @@ pages=Səhifələr
loading=Yüklənir... loading=Yüklənir...
addToDoc=Sənədə Əlavə Et addToDoc=Sənədə Əlavə Et
reset=Sıfırla reset=Sıfırla
apply=Apply
legal.privacy=Məxfilik Siyasəti legal.privacy=Məxfilik Siyasəti
legal.terms=Qaydalar və Şərtlər legal.terms=Qaydalar və Şərtlər
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=Fayl Tapılmadı database.fileNotFound=Fayl Tapılmadı
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
database.failedImportFile=Faylı daxil etmək alınmadı database.failedImportFile=Faylı daxil etmək alınmadı
database.notSupported=This function is not available for your database connection.
session.expired=Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin. session.expired=Sessiyanızın vaxtı bitdi. Səhifəni yeniləyin və yenidən cəhd edin.
session.refreshPage=Səhifəni Yenilə session.refreshPage=Səhifəni Yenilə
@@ -476,10 +474,6 @@ home.autoRedact.title=Avtomatik Gizlətmə
home.autoRedact.desc=Daxil edilmiş data əsasında PDF-dəki müəyyən mətn hissəsini qara qutu ilə gizlədir home.autoRedact.desc=Daxil edilmiş data əsasında PDF-dəki müəyyən mətn hissəsini qara qutu ilə gizlədir
autoRedact.tags=Qarala,gizlət,sil,qara,marker,gizli autoRedact.tags=Qarala,gizlət,sil,qara,marker,gizli
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF-dən CSV-ə home.tableExtraxt.title=PDF-dən CSV-ə
home.tableExtraxt.desc=PDF-dən cədvəlləri CSV-ə çevirərək xaric edir home.tableExtraxt.desc=PDF-dən cədvəlləri CSV-ə çevirərək xaric edir
tableExtraxt.tags=CSV,Cədvəl xaricetmə,xaric et,çevir tableExtraxt.tags=CSV,Cədvəl xaricetmə,xaric et,çevir
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=Etibarsız Sorğu
login.oauth2AccessDenied=Giriş rədd edildi login.oauth2AccessDenied=Giriş rədd edildi
login.oauth2InvalidTokenResponse=Etibarsız Token Cavabı login.oauth2InvalidTokenResponse=Etibarsız Token Cavabı
login.oauth2InvalidIdToken=Etibarsız Id Token login.oauth2InvalidIdToken=Etibarsız Id Token
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=İstifadəçi deaktivləşdirilmişdir, bu istifadəçi adı ilə giriş hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın. login.userIsDisabled=İstifadəçi deaktivləşdirilmişdir, bu istifadəçi adı ilə giriş hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
login.alreadyLoggedIn=Siz artıq daxil olmusunuz login.alreadyLoggedIn=Siz artıq daxil olmusunuz
login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin. login.alreadyLoggedIn2=cihazlar. Zəhmət olmasa, cihazlardan çıxış edin və yenidən cəhd edin.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=Fərdi Əlavə Başlıq
autoRedact.convertPDFToImageLabel=PDF-i PDF-Şəkil-ə çevir (Qutunun arxasındakı yazını silmək üçün istifadə edilir) autoRedact.convertPDFToImageLabel=PDF-i PDF-Şəkil-ə çevir (Qutunun arxasındakı yazını silmək üçün istifadə edilir)
autoRedact.submitButton=Təsdiqlə autoRedact.submitButton=Təsdiqlə
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=Javascripti Göstər showJS.title=Javascripti Göstər
@@ -862,8 +830,6 @@ sign.first=İlk səhifə
sign.last=Son səhifə sign.last=Son səhifə
sign.next=Növbəti səhifə sign.next=Növbəti səhifə
sign.previous=Əvvəlki səhifə sign.previous=Əvvəlki səhifə
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=Bərpa Et repair.title=Bərpa Et
@@ -1075,7 +1041,6 @@ pdfToImage.grey=Boz Tonlama
pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər) pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər)
pdfToImage.submit=Çevir pdfToImage.submit=Çevir
pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=PDF-i Ayır
fileChooser.click=Click fileChooser.click=Click
fileChooser.or=or fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop fileChooser.dragAndDrop=Drag & Drop
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes

View File

@@ -82,7 +82,6 @@ pages=Страници
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset reset=Reset
apply=Apply
legal.privacy=Политика за поверителност legal.privacy=Политика за поверителност
legal.terms=Правила и условия legal.terms=Правила и условия
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=Файлът не е намерен database.fileNotFound=Файлът не е намерен
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
database.failedImportFile=Неуспешно импортиране на файл database.failedImportFile=Неуспешно импортиране на файл
database.notSupported=This function is not available for your database connection.
session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново. session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
session.refreshPage=Refresh Page session.refreshPage=Refresh Page
@@ -476,10 +474,6 @@ home.autoRedact.title=Автоматично редактиране
home.autoRedact.desc=Автоматично редактира (зачернява) текст в PDF въз основа на въведен текст home.autoRedact.desc=Автоматично редактира (зачернява) текст в PDF въз основа на въведен текст
autoRedact.tags=Редактиране,Скриване,затъмняване,черен,маркер,скрит autoRedact.tags=Редактиране,Скриване,затъмняване,черен,маркер,скрит
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF в CSV home.tableExtraxt.title=PDF в CSV
home.tableExtraxt.desc=Извлича таблици от PDF, като ги конвертира в CSV home.tableExtraxt.desc=Извлича таблици от PDF, като ги конвертира в CSV
tableExtraxt.tags=CSV,извличане на таблица,извличане,конвертиране tableExtraxt.tags=CSV,извличане на таблица,извличане,конвертиране
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=Невалидна заявка
login.oauth2AccessDenied=Отказан достъп login.oauth2AccessDenied=Отказан достъп
login.oauth2InvalidTokenResponse=Невалиден отговор на токена login.oauth2InvalidTokenResponse=Невалиден отговор на токена
login.oauth2InvalidIdToken=Невалиден токен за идентификатор login.oauth2InvalidIdToken=Невалиден токен за идентификатор
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора. login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
login.alreadyLoggedIn=Вече сте влезли в login.alreadyLoggedIn=Вече сте влезли в
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново. login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=Персонализирана допълните
autoRedact.convertPDFToImageLabel=Преобразуване на PDF към PDF-изображение (използва се за премахване на текст зад полето) autoRedact.convertPDFToImageLabel=Преобразуване на PDF към PDF-изображение (използва се за премахване на текст зад полето)
autoRedact.submitButton=Изпращане autoRedact.submitButton=Изпращане
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=Покажи Javascript showJS.title=Покажи Javascript
@@ -862,8 +830,6 @@ sign.first=First page
sign.last=Last page sign.last=Last page
sign.next=Next page sign.next=Next page
sign.previous=Previous page sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=Поправи repair.title=Поправи
@@ -1075,7 +1041,6 @@ pdfToImage.grey=Скала на сивото
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!) pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
pdfToImage.submit=Преобразуване pdfToImage.submit=Преобразуване
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP. pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=Разделяне на PDF
fileChooser.click=Click fileChooser.click=Click
fileChooser.or=or fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop fileChooser.dragAndDrop=Drag & Drop
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes

View File

@@ -82,7 +82,6 @@ pages=Pàgines
loading=Carregant... loading=Carregant...
addToDoc=Afegeix al document addToDoc=Afegeix al document
reset=Reset reset=Reset
apply=Apply
legal.privacy=Política de Privacitat legal.privacy=Política de Privacitat
legal.terms=Termes i condicions legal.terms=Termes i condicions
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=Fitxer no trobat database.fileNotFound=Fitxer no trobat
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
database.failedImportFile=Error en la importació del fitxer database.failedImportFile=Error en la importació del fitxer
database.notSupported=This function is not available for your database connection.
session.expired=La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho. session.expired=La teva sessió ha expirat. Si us plau, actualitza la pàgina i torna a intentar-ho.
session.refreshPage=Actualitza la pàgina session.refreshPage=Actualitza la pàgina
@@ -476,10 +474,6 @@ home.autoRedact.title=Redacció Automàtica
home.autoRedact.desc=Redacta automàticament (enfosqueix) text en un PDF basat en el text introduït home.autoRedact.desc=Redacta automàticament (enfosqueix) text en un PDF basat en el text introduït
autoRedact.tags=Redact,Hide,black out,black,marker,hidden autoRedact.tags=Redact,Hide,black out,black,marker,hidden
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF a CSV home.tableExtraxt.title=PDF a CSV
home.tableExtraxt.desc=Extreu taules d'un PDF convertint-les a CSV home.tableExtraxt.desc=Extreu taules d'un PDF convertint-les a CSV
tableExtraxt.tags=CSV,Table Extraction,extract,convert tableExtraxt.tags=CSV,Table Extraction,extract,convert
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=Sol·licitud no vàlida
login.oauth2AccessDenied=Accés denegat login.oauth2AccessDenied=Accés denegat
login.oauth2InvalidTokenResponse=Resposta de token no vàlida login.oauth2InvalidTokenResponse=Resposta de token no vàlida
login.oauth2InvalidIdToken=ID Token no vàlid login.oauth2InvalidIdToken=ID Token no vàlid
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=L'usuari està desactivat, l'inici de sessió està actualment bloquejat amb aquest nom d'usuari. Si us plau, contacta amb l'administrador. login.userIsDisabled=L'usuari està desactivat, l'inici de sessió està actualment bloquejat amb aquest nom d'usuari. Si us plau, contacta amb l'administrador.
login.alreadyLoggedIn=Ja has iniciat sessió a login.alreadyLoggedIn=Ja has iniciat sessió a
login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar. login.alreadyLoggedIn2=dispositius. Si us plau, tanca la sessió en els dispositius i torna-ho a intentar.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=Espai Extra Personalitzat
autoRedact.convertPDFToImageLabel=Converteix PDF a Imatge PDF (S'utilitza per eliminar text darrere del quadre) autoRedact.convertPDFToImageLabel=Converteix PDF a Imatge PDF (S'utilitza per eliminar text darrere del quadre)
autoRedact.submitButton=Envia autoRedact.submitButton=Envia
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=Mostra Javascript showJS.title=Mostra Javascript
@@ -862,8 +830,6 @@ sign.first=First page
sign.last=Last page sign.last=Last page
sign.next=Next page sign.next=Next page
sign.previous=Previous page sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=Reparar repair.title=Reparar
@@ -1075,7 +1041,6 @@ pdfToImage.grey=Escala de Grisos
pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!) pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!)
pdfToImage.submit=Converteix pdfToImage.submit=Converteix
pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP. pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP.
pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=Divideix PDF
fileChooser.click=Click fileChooser.click=Click
fileChooser.or=or fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop fileChooser.dragAndDrop=Drag & Drop
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes

View File

@@ -82,7 +82,6 @@ pages=Strany
loading=Načítání... loading=Načítání...
addToDoc=Přidat do dokumentu addToDoc=Přidat do dokumentu
reset=Reset reset=Reset
apply=Apply
legal.privacy=Politika soukromí legal.privacy=Politika soukromí
legal.terms=Podmínky použití legal.terms=Podmínky použití
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=File not Found database.fileNotFound=File not Found
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
database.failedImportFile=Failed Import File database.failedImportFile=Failed Import File
database.notSupported=This function is not available for your database connection.
session.expired=Vaše sesace vypršela. Prosím obnovte stránku a zkusit to znovu. session.expired=Vaše sesace vypršela. Prosím obnovte stránku a zkusit to znovu.
session.refreshPage=Refresh Page session.refreshPage=Refresh Page
@@ -476,10 +474,6 @@ home.autoRedact.title=Automatické odstranění
home.autoRedact.desc=Automaticky zakrývá text v PDF na základě vstupního textu home.autoRedact.desc=Automaticky zakrývá text v PDF na základě vstupního textu
autoRedact.tags=Odstranit,Skrytý,černý,zakrýt,značka,skrytý autoRedact.tags=Odstranit,Skrytý,černý,zakrýt,značka,skrytý
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF na CSV home.tableExtraxt.title=PDF na CSV
home.tableExtraxt.desc=Extrahuje tabulky z PDF a konvertuje je do formátu CSV home.tableExtraxt.desc=Extrahuje tabulky z PDF a konvertuje je do formátu CSV
tableExtraxt.tags=CSV,Extrakce tabulky,extrahovat,konvertovat tableExtraxt.tags=CSV,Extrakce tabulky,extrahovat,konvertovat
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=Neplatný požadavek
login.oauth2AccessDenied=Přístup zazobán login.oauth2AccessDenied=Přístup zazobán
login.oauth2InvalidTokenResponse=Neplatná odpověď tokenu login.oauth2InvalidTokenResponse=Neplatná odpověď tokenu
login.oauth2InvalidIdToken=Neplatný identifikační token login.oauth2InvalidIdToken=Neplatný identifikační token
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=Uživatel je deaktivován, přihlášení aktuálně pro tuto uživatelskou jmena je zakázáno. Kontaktujte správce. login.userIsDisabled=Uživatel je deaktivován, přihlášení aktuálně pro tuto uživatelskou jmena je zakázáno. Kontaktujte správce.
login.alreadyLoggedIn=Jste již přihlášeni na login.alreadyLoggedIn=Jste již přihlášeni na
login.alreadyLoggedIn2=zariadení. Odhlašujte se z těchto zařízení a zkuste to znovu. login.alreadyLoggedIn2=zariadení. Odhlašujte se z těchto zařízení a zkuste to znovu.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=Vlastní doplňující vzdálenost
autoRedact.convertPDFToImageLabel=Převést PDF do PDF-Obrázku (Pro odstranění textu za obdélníkem) autoRedact.convertPDFToImageLabel=Převést PDF do PDF-Obrázku (Pro odstranění textu za obdélníkem)
autoRedact.submitButton=Odeslat autoRedact.submitButton=Odeslat
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=Zobrazit JavaScript showJS.title=Zobrazit JavaScript
@@ -862,8 +830,6 @@ sign.first=First page
sign.last=Last page sign.last=Last page
sign.next=Next page sign.next=Next page
sign.previous=Previous page sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=Opravit repair.title=Opravit
@@ -1075,7 +1041,6 @@ pdfToImage.grey=Stupně šedi
pdfToImage.blackwhite=Černobílý (Může dojít k ztrátě dat!) pdfToImage.blackwhite=Černobílý (Může dojít k ztrátě dat!)
pdfToImage.submit=Převést pdfToImage.submit=Převést
pdfToImage.info=Python není nainstalován. Potřebuje se pro konverzi do WebP. pdfToImage.info=Python není nainstalován. Potřebuje se pro konverzi do WebP.
pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=Podělit se PDF
fileChooser.click=Click fileChooser.click=Click
fileChooser.or=or fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop fileChooser.dragAndDrop=Drag & Drop
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes

View File

@@ -82,7 +82,6 @@ pages=Sideantal
loading=Laster... loading=Laster...
addToDoc=Tilføj til Dokument addToDoc=Tilføj til Dokument
reset=Reset reset=Reset
apply=Apply
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Vilkår og betingelser legal.terms=Vilkår og betingelser
@@ -249,7 +248,6 @@ database.backupCreated=Database backup successful
database.fileNotFound=Fil ikke fundet database.fileNotFound=Fil ikke fundet
database.fileNullOrEmpty=Fil må ikke være null eller tom database.fileNullOrEmpty=Fil må ikke være null eller tom
database.failedImportFile=Kunne ikke importere fil database.failedImportFile=Kunne ikke importere fil
database.notSupported=This function is not available for your database connection.
session.expired=Din sesions tid har udløbet. Genlad siden og prøv igen. session.expired=Din sesions tid har udløbet. Genlad siden og prøv igen.
session.refreshPage=Refresh Page session.refreshPage=Refresh Page
@@ -476,10 +474,6 @@ home.autoRedact.title=Auto Rediger
home.autoRedact.desc=Auto Redigerer (Sværter) tekst i en PDF baseret på input tekst home.autoRedact.desc=Auto Redigerer (Sværter) tekst i en PDF baseret på input tekst
autoRedact.tags=Rediger,Skjul,sværte,sort,markør,skjult autoRedact.tags=Rediger,Skjul,sværte,sort,markør,skjult
home.redact.title=Manual Redaction
home.redact.desc=Redacts a PDF based on selected text, drawn shapes and/or selected page(s)
redact.tags=Redact,Hide,black out,black,marker,hidden,manual
home.tableExtraxt.title=PDF til CSV home.tableExtraxt.title=PDF til CSV
home.tableExtraxt.desc=Udtrækker Tabeller fra en PDF og konverterer dem til CSV home.tableExtraxt.desc=Udtrækker Tabeller fra en PDF og konverterer dem til CSV
tableExtraxt.tags=CSV,Tabeludtrækning,udtræk,konvertér tableExtraxt.tags=CSV,Tabeludtrækning,udtræk,konvertér
@@ -567,7 +561,6 @@ login.oauth2invalidRequest=Ugyldig Anmodning
login.oauth2AccessDenied=Adgang Nægtet login.oauth2AccessDenied=Adgang Nægtet
login.oauth2InvalidTokenResponse=Ugyldigt Token Svar login.oauth2InvalidTokenResponse=Ugyldigt Token Svar
login.oauth2InvalidIdToken=Ugyldigt Id Token login.oauth2InvalidIdToken=Ugyldigt Id Token
login.relyingPartyRegistrationNotFound=No relying party registration found
login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med dette brugernavn. Kontakt venligst administratoren. login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med dette brugernavn. Kontakt venligst administratoren.
login.alreadyLoggedIn=Du er allerede logget ind på login.alreadyLoggedIn=Du er allerede logget ind på
login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen. login.alreadyLoggedIn2=enheder. Log ud af disse enheder og prøv igen.
@@ -585,31 +578,6 @@ autoRedact.customPaddingLabel=Brugerdefineret Ekstra Polstring
autoRedact.convertPDFToImageLabel=Konvertér PDF til PDF-Billede (Bruges til at fjerne tekst bag boksen) autoRedact.convertPDFToImageLabel=Konvertér PDF til PDF-Billede (Bruges til at fjerne tekst bag boksen)
autoRedact.submitButton=Indsend autoRedact.submitButton=Indsend
#redact
redact.title=Manual Redaction
redact.header=Manual Redaction
redact.submit=Redact
redact.textBasedRedaction=Text based Redaction
redact.pageBasedRedaction=Page-based Redaction
redact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
redact.pageRedactionNumbers.title=Pages
redact.pageRedactionNumbers.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1)
redact.redactionColor.title=Redaction Color
redact.export=Export
redact.upload=Upload
redact.boxRedaction=Box draw redaction
redact.zoom=Zoom
redact.zoomIn=Zoom in
redact.zoomOut=Zoom out
redact.nextPage=Next Page
redact.previousPage=Previous Page
redact.toggleSidebar=Toggle Sidebar
redact.showThumbnails=Show Thumbnails
redact.showDocumentOutline=Show Document Outline (double-click to expand/collapse all items)
redact.showAttatchments=Show Attachments
redact.showLayers=Show Layers (double-click to reset all layers to the default state)
redact.colourPicker=Colour Picker
redact.findCurrentOutlineItem=Find current outline item
#showJS #showJS
showJS.title=Vis Javascript showJS.title=Vis Javascript
@@ -862,8 +830,6 @@ sign.first=First page
sign.last=Last page sign.last=Last page
sign.next=Next page sign.next=Next page
sign.previous=Previous page sign.previous=Previous page
sign.maintainRatio=Toggle maintain aspect ratio
#repair #repair
repair.title=Reparér repair.title=Reparér
@@ -1075,7 +1041,6 @@ pdfToImage.grey=Gråtone
pdfToImage.blackwhite=Sort og Hvid (Kan miste data!) pdfToImage.blackwhite=Sort og Hvid (Kan miste data!)
pdfToImage.submit=Konvertér pdfToImage.submit=Konvertér
pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering. pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering.
pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1)
#addPassword #addPassword
@@ -1319,8 +1284,6 @@ splitByChapters.submit=Splitter PDF
fileChooser.click=Click fileChooser.click=Click
fileChooser.or=or fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop fileChooser.dragAndDrop=Drag & Drop
fileChooser.dragAndDropPDF=Drag & Drop PDF file
fileChooser.dragAndDropImage=Drag & Drop Image file
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes

Some files were not shown because too many files have changed in this diff Show More