Compare commits
52 Commits
migrate-en
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c7b83ee87 | ||
|
|
9152e64b9f | ||
|
|
96655f7cac | ||
|
|
8f7153b30a | ||
|
|
366bec602d | ||
|
|
c9c8378fe0 | ||
|
|
7a7338c6de | ||
|
|
77dec10f25 | ||
|
|
12b03be2be | ||
|
|
222c18cdae | ||
|
|
d2bc281e42 | ||
|
|
ac10c9fa43 | ||
|
|
4fabc07a44 | ||
|
|
2ab951e080 | ||
|
|
a1f7bb3e4a | ||
|
|
f64d7d42d9 | ||
|
|
300011f9b6 | ||
|
|
e328833f02 | ||
|
|
4c701b2e69 | ||
|
|
16295c7bb9 | ||
|
|
69da443096 | ||
|
|
ee6fbdd61f | ||
|
|
a5da7fc2c3 | ||
|
|
1f92318df4 | ||
|
|
9df5e2aca0 | ||
|
|
6aad45fcec | ||
|
|
ec5018cc80 | ||
|
|
2404ceb46e | ||
|
|
f23a2d508f | ||
|
|
f5ca02df1d | ||
|
|
1e29cf43fb | ||
|
|
ebfe00717d | ||
|
|
063a7244a3 | ||
|
|
7b9e52dec6 | ||
|
|
296dbc5dea | ||
|
|
f6654a06d9 | ||
|
|
219adb99ec | ||
|
|
23a7b11a74 | ||
|
|
6ac804e994 | ||
|
|
09ff207888 | ||
|
|
880ca6af7f | ||
|
|
3797a6827c | ||
|
|
0f5d95661c | ||
|
|
d5aea7f0b6 | ||
|
|
0412263d01 | ||
|
|
d3d9158641 | ||
|
|
c785fb20d1 | ||
|
|
ac09831654 | ||
|
|
487a82eb65 | ||
|
|
0f5db746c7 | ||
|
|
139faf4eba | ||
|
|
167c85e73f |
23
.github/scripts/check_language_properties.py
vendored
23
.github/scripts/check_language_properties.py
vendored
@@ -164,7 +164,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
|||||||
if current_entry["type"] == "entry":
|
if current_entry["type"] == "entry":
|
||||||
if ref_entry_copy["type"] != "entry":
|
if ref_entry_copy["type"] != "entry":
|
||||||
continue
|
continue
|
||||||
if ref_entry_copy["key"] == current_entry["key"]:
|
if ref_entry_copy["key"].lower() == current_entry["key"].lower():
|
||||||
ref_entry_copy["value"] = current_entry["value"]
|
ref_entry_copy["value"] = current_entry["value"]
|
||||||
updated_properties.append(ref_entry_copy)
|
updated_properties.append(ref_entry_copy)
|
||||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||||
@@ -199,29 +199,30 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
|||||||
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
|
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
|
||||||
|
|
||||||
for file_path in file_arr:
|
for file_path in file_arr:
|
||||||
absolute_path = os.path.abspath(file_path)
|
file_normpath = os.path.normpath(file_path)
|
||||||
|
absolute_path = os.path.abspath(file_normpath)
|
||||||
# Verify that file is within the expected directory
|
# Verify that file is within the expected directory
|
||||||
if not absolute_path.startswith(base_dir):
|
if not absolute_path.startswith(base_dir):
|
||||||
raise ValueError(f"Unsafe file found: {file_path}")
|
raise ValueError(f"Unsafe file found: {file_normpath}")
|
||||||
# Verify file size before processing
|
# Verify file size before processing
|
||||||
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
|
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"The file {file_path} is too large and could pose a security risk."
|
f"The file {file_normpath} is too large and could pose a security risk."
|
||||||
)
|
)
|
||||||
|
|
||||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
|
||||||
if (
|
if (
|
||||||
basename_current_file == basename_reference_file
|
basename_current_file == basename_reference_file
|
||||||
or (
|
or (
|
||||||
# only local windows command
|
# only local windows command
|
||||||
not file_path.startswith(
|
not file_normpath.startswith(
|
||||||
os.path.join("", "src", "main", "resources", "messages_")
|
os.path.join("", "src", "main", "resources", "messages_")
|
||||||
)
|
)
|
||||||
and not file_path.startswith(
|
and not file_normpath.startswith(
|
||||||
os.path.join(os.getcwd(), "src", "main", "resources", "messages_")
|
os.path.join(os.getcwd(), "src", "main", "resources", "messages_")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or not file_path.endswith(".properties")
|
or not file_normpath.endswith(".properties")
|
||||||
or not basename_current_file.startswith("messages_")
|
or not basename_current_file.startswith("messages_")
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
@@ -292,13 +293,13 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
|||||||
else:
|
else:
|
||||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||||
|
|
||||||
if find_duplicate_keys(os.path.join(branch, file_path)):
|
if find_duplicate_keys(os.path.join(branch, file_normpath)):
|
||||||
has_differences = True
|
has_differences = True
|
||||||
output = "\n".join(
|
output = "\n".join(
|
||||||
[
|
[
|
||||||
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
|
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
|
||||||
for key, first, duplicate in find_duplicate_keys(
|
for key, first, duplicate in find_duplicate_keys(
|
||||||
os.path.join(branch, file_path)
|
os.path.join(branch, file_normpath)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
2
.github/workflows/PR-Demo-Comment.yml
vendored
2
.github/workflows/PR-Demo-Comment.yml
vendored
@@ -121,7 +121,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push PR-specific image
|
- name: Build and push PR-specific image
|
||||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Test Reports
|
- name: Upload Test Reports
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: test-reports-jdk-${{ matrix.jdk-version }}
|
name: test-reports-jdk-${{ matrix.jdk-version }}
|
||||||
path: |
|
path: |
|
||||||
@@ -80,7 +80,7 @@ jobs:
|
|||||||
|
|
||||||
- name: FAILED - check the licenses for compatibility
|
- name: FAILED - check the licenses for compatibility
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: dependencies-without-allowed-license.json
|
name: dependencies-without-allowed-license.json
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
6
.github/workflows/licenses-update.yml
vendored
6
.github/workflows/licenses-update.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3
|
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
- name: FAILED - check the licenses for compatibility
|
- name: FAILED - check the licenses for compatibility
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: dependencies-without-allowed-license.json
|
name: dependencies-without-allowed-license.json
|
||||||
path: |
|
path: |
|
||||||
@@ -69,7 +69,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@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: "Update 3rd Party Licenses"
|
commit-message: "Update 3rd Party Licenses"
|
||||||
|
|||||||
10
.github/workflows/multiOSReleases.yml
vendored
10
.github/workflows/multiOSReleases.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
|||||||
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -114,7 +114,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
run: ls -R ./binaries
|
run: ls -R ./binaries
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -224,7 +224,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
|
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
if: matrix.os == 'windows-latest'
|
if: matrix.os == 'windows-latest'
|
||||||
@@ -255,7 +255,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
4
.github/workflows/pre_commit.yml
vendored
4
.github/workflows/pre_commit.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3
|
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
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@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: ":file_folder: pre-commit"
|
commit-message: ":file_folder: pre-commit"
|
||||||
|
|||||||
8
.github/workflows/push-docker.yml
vendored
8
.github/workflows/push-docker.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
|
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.4.1"
|
cosign-release: "v2.4.1"
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
id: build-push-regular
|
id: build-push-regular
|
||||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
id: build-push-lite
|
id: build-push-lite
|
||||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -166,7 +166,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
id: build-push-fat
|
id: build-push-fat
|
||||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
|||||||
6
.github/workflows/releaseArtifacts.yml
vendored
6
.github/workflows/releaseArtifacts.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
ls -R ./build/launch4j
|
ls -R ./build/launch4j
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: binaries${{ matrix.file_suffix }}
|
name: binaries${{ matrix.file_suffix }}
|
||||||
path: |
|
path: |
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
run: ls -R
|
run: ls -R
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
|
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
||||||
|
|
||||||
- name: Generate key pair
|
- name: Generate key pair
|
||||||
run: cosign generate-key-pair
|
run: cosign generate-key-pair
|
||||||
@@ -139,7 +139,7 @@ jobs:
|
|||||||
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
- name: Upload signed artifacts
|
- name: Upload signed artifacts
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: signed${{ matrix.file_suffix }}
|
name: signed${{ matrix.file_suffix }}
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
6
.github/workflows/scorecards.yml
vendored
6
.github/workflows/scorecards.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: "Run analysis"
|
- name: "Run analysis"
|
||||||
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
|
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||||
# format to the repository Actions tab.
|
# format to the repository Actions tab.
|
||||||
- name: "Upload artifact"
|
- name: "Upload artifact"
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
@@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
|
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
4
.github/workflows/sonarqube.yml
vendored
4
.github/workflows/sonarqube.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Problems Report on Failure
|
- name: Upload Problems Report on Failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: gradle-problems-report
|
name: gradle-problems-report
|
||||||
path: build/reports/problems/problems-report.html
|
path: build/reports/problems/problems-report.html
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Sonar Logs on Failure
|
- name: Upload Sonar Logs on Failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||||
with:
|
with:
|
||||||
name: sonar-logs
|
name: sonar-logs
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
6
.github/workflows/sync_files.yml
vendored
6
.github/workflows/sync_files.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3
|
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.GH_APP_ID }}
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate GitHub App Token
|
- name: Generate GitHub App Token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@67e27a7eb7db372a1c61a7f9bdab8699e9ee57f7 # v1.11.3
|
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
|
||||||
with:
|
with:
|
||||||
app-id: ${{ vars.GH_APP_ID }}
|
app-id: ${{ vars.GH_APP_ID }}
|
||||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "no changes"
|
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || 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@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
commit-message: Update files
|
commit-message: Update files
|
||||||
|
|||||||
2
.github/workflows/testdriver.yml
vendored
2
.github/workflows/testdriver.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push test image
|
- name: Build and push test image
|
||||||
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
|
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ COPY . .
|
|||||||
|
|
||||||
# Build the application with DOCKER_ENABLE_SECURITY=false
|
# Build the application with DOCKER_ENABLE_SECURITY=false
|
||||||
RUN DOCKER_ENABLE_SECURITY=true \
|
RUN DOCKER_ENABLE_SECURITY=true \
|
||||||
STIRLING_PDF_DESKTOP_UI=false \
|
STIRLING_PDF_DESKTOP_UI=false \
|
||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
|
|
||||||
# Main stage
|
# Main stage
|
||||||
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
|
||||||
@@ -50,29 +50,29 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
|
||||||
apk upgrade --no-cache -a && \
|
apk upgrade --no-cache -a && \
|
||||||
apk add --no-cache \
|
apk add --no-cache \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
tzdata \
|
tzdata \
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
openssl-dev \
|
openssl-dev \
|
||||||
openjdk21-jre \
|
openjdk21-jre \
|
||||||
# Doc conversion
|
# Doc conversion
|
||||||
gcompat \
|
gcompat \
|
||||||
libc6-compat \
|
libc6-compat \
|
||||||
libreoffice \
|
libreoffice \
|
||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
qpdf \
|
qpdf \
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
|
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
python3 \
|
python3 \
|
||||||
py3-pip \
|
py3-pip \
|
||||||
py3-pillow@testing \
|
py3-pillow@testing \
|
||||||
@@ -89,7 +89,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
fc-cache -f -v && \
|
fc-cache -f -v && \
|
||||||
chmod +x /scripts/* && \
|
chmod +x /scripts/* && \
|
||||||
chmod +x /scripts/init.sh && \
|
chmod +x /scripts/init.sh && \
|
||||||
# User permissions
|
# User permissions
|
||||||
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
|
||||||
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
|
||||||
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
chown stirlingpdfuser:stirlingpdfgroup /app.jar
|
||||||
@@ -97,4 +97,4 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
|
|||||||
EXPOSE 8080/tcp
|
EXPOSE 8080/tcp
|
||||||
# Set user and run command
|
# Set user and run command
|
||||||
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
|
||||||
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]
|
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ Fork Stirling-PDF and create a new branch out of `main`.
|
|||||||
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
|
Then add a reference to the language in the navbar by adding a new language entry to the dropdown:
|
||||||
|
|
||||||
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
|
- Edit the file: [languages.html](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html)
|
||||||
- Add a flag SVG file to: [flags directory](https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags)
|
|
||||||
|
|
||||||
Any SVG flags are fine; most of the current ones were sourced from [here](https://flagicons.lipis.dev/). If your language isn't represented by a flag, choose a similar one, such as Saudi Arabia's flag for Arabic.
|
|
||||||
|
|
||||||
For example, to add Polish, you would add:
|
For example, to add Polish, you would add:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<a th:if="${#lists.isEmpty(@languages) or #lists.contains(@languages, 'pl_PL')}" class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL"> <img th:src="@{'/images/flags/pl.svg'}" alt="icon" width="20" height="15"> Polski</a>
|
<div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}" ></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
The `data-bs-language-code` is the code used to reference the file in the next step.
|
The `data-bs-language-code` is the code used to reference the file in the next step.
|
||||||
|
|||||||
@@ -148,11 +148,11 @@ Stirling-PDF currently supports 39 languages!
|
|||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
| Tibetan (བོད་ཡིག་) (zh_BO) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|||||||
19
build.gradle
19
build.gradle
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
id "java"
|
||||||
id "org.springframework.boot" version "3.4.1"
|
id "org.springframework.boot" version "3.4.3"
|
||||||
id "io.spring.dependency-management" version "1.1.7"
|
id "io.spring.dependency-management" version "1.1.7"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
@@ -15,17 +15,17 @@ plugins {
|
|||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.4.2"
|
springBootVersion = "3.4.3"
|
||||||
pdfboxVersion = "3.0.4"
|
pdfboxVersion = "3.0.4"
|
||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.12.0"
|
||||||
lombokVersion = "1.18.36"
|
lombokVersion = "1.18.36"
|
||||||
bouncycastleVersion = "1.80"
|
bouncycastleVersion = "1.80"
|
||||||
springSecuritySamlVersion = "6.4.2"
|
springSecuritySamlVersion = "6.4.3"
|
||||||
openSamlVersion = "4.3.2"
|
openSamlVersion = "4.3.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.42.0"
|
version = "0.43.1"
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
@@ -294,8 +294,8 @@ configurations.all {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
//tmp for security bumps
|
//tmp for security bumps
|
||||||
implementation 'ch.qos.logback:logback-core:1.5.15'
|
implementation 'ch.qos.logback:logback-core:1.5.17'
|
||||||
implementation 'ch.qos.logback:logback-classic:1.5.15'
|
implementation 'ch.qos.logback:logback-classic:1.5.17'
|
||||||
|
|
||||||
|
|
||||||
// Exclude vulnerable BouncyCastle version used in tableau
|
// Exclude vulnerable BouncyCastle version used in tableau
|
||||||
@@ -306,8 +306,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
|
||||||
implementation 'org.apache.commons:commons-compress:1.26.0'
|
implementation "me.friwi:jcefmaven:132.3.1"
|
||||||
implementation "me.friwi:jcefmaven:127.3.1"
|
|
||||||
implementation "org.openjfx:javafx-controls:21"
|
implementation "org.openjfx:javafx-controls:21"
|
||||||
implementation "org.openjfx:javafx-swing:21"
|
implementation "org.openjfx:javafx-swing:21"
|
||||||
}
|
}
|
||||||
@@ -332,7 +331,7 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.session:spring-session-core:3.4.1"
|
implementation "org.springframework.session:spring-session-core:3.4.2"
|
||||||
implementation "org.springframework:spring-jdbc:6.2.3"
|
implementation "org.springframework:spring-jdbc:6.2.3"
|
||||||
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
@@ -348,8 +347,8 @@ dependencies {
|
|||||||
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
||||||
implementation 'com.coveo:saml-client:5.0.0'
|
implementation 'com.coveo:saml-client:5.0.0'
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
implementation 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||||
|
|
||||||
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.springframework.core.Ordered;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.posthog.java.shaded.org.json.JSONObject;
|
import com.posthog.java.shaded.org.json.JSONObject;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class KeygenLicenseVerifier {
|
public class KeygenLicenseVerifier {
|
||||||
|
// todo: place in config files?
|
||||||
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
||||||
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
||||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
@@ -67,7 +69,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error verifying license: " + e.getMessage());
|
log.error("Error verifying license: {}", e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,10 +96,9 @@ 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.debug("ValidateLicenseResponse body: {}", response.body());
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
|
|
||||||
JsonNode metaNode = jsonResponse.path("meta");
|
JsonNode metaNode = jsonResponse.path("meta");
|
||||||
boolean isValid = metaNode.path("valid").asBoolean();
|
boolean isValid = metaNode.path("valid").asBoolean();
|
||||||
|
|
||||||
@@ -119,7 +120,7 @@ public class KeygenLicenseVerifier {
|
|||||||
log.info(applicationProperties.toString());
|
log.info(applicationProperties.toString());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.error("Error validating license. Status code: " + response.statusCode());
|
log.error("Error validating license. Status code: {}", response.statusCode());
|
||||||
}
|
}
|
||||||
return jsonResponse;
|
return jsonResponse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.Scheduled;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ public class LicenseKeyChecker {
|
|||||||
|
|
||||||
public void updateLicenseKey(String newKey) throws IOException {
|
public void updateLicenseKey(String newKey) throws IOException {
|
||||||
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
||||||
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
|
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
|
||||||
checkLicense();
|
checkLicense();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -22,11 +21,14 @@ import io.github.pixee.security.SystemCommand;
|
|||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import jakarta.annotation.PreDestroy;
|
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.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@@ -62,6 +64,12 @@ public class SPDFApplication {
|
|||||||
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");
|
||||||
|
|
||||||
|
int desiredPort = 8080;
|
||||||
|
String port = UrlUtils.findAvailablePort(desiredPort);
|
||||||
|
props.put("server.port", port);
|
||||||
|
System.setProperty("server.port", port);
|
||||||
|
log.info("Desktop UI mode: Using port {}", port);
|
||||||
}
|
}
|
||||||
|
|
||||||
app.setAdditionalProfiles(getActiveProfile(args));
|
app.setAdditionalProfiles(getActiveProfile(args));
|
||||||
@@ -75,18 +83,18 @@ public class SPDFApplication {
|
|||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
|
|
||||||
// External config files
|
// External config files
|
||||||
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
|
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||||
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
|
log.info("Settings file: {}", settingsPath.toString());
|
||||||
|
if (Files.exists(settingsPath)) {
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location", "file:" + settingsPath.toString());
|
||||||
"file:" + InstallationPathConfig.getSettingsPath());
|
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
|
||||||
"External configuration file '{}' does not exist.",
|
|
||||||
InstallationPathConfig.getSettingsPath());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
|
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||||
|
log.info("Custom settings file: {}", customSettingsPath.toString());
|
||||||
|
if (Files.exists(customSettingsPath)) {
|
||||||
String existingLocation =
|
String existingLocation =
|
||||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||||
if (!existingLocation.isEmpty()) {
|
if (!existingLocation.isEmpty()) {
|
||||||
@@ -94,11 +102,11 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
propertyFiles.put(
|
propertyFiles.put(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
|
existingLocation + "file:" + customSettingsPath.toString());
|
||||||
} else {
|
} else {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Custom configuration file '{}' does not exist.",
|
"Custom configuration file '{}' does not exist.",
|
||||||
InstallationPathConfig.getCustomSettingsPath());
|
customSettingsPath.toString());
|
||||||
}
|
}
|
||||||
Properties finalProps = new Properties();
|
Properties finalProps = new Properties();
|
||||||
|
|
||||||
@@ -120,7 +128,7 @@ public class SPDFApplication {
|
|||||||
try {
|
try {
|
||||||
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
|
||||||
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
log.error("Error creating directories: {}", e.getMessage());
|
log.error("Error creating directories: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +157,7 @@ public class SPDFApplication {
|
|||||||
} else if (os.contains("nix") || os.contains("nux")) {
|
} else if (os.contains("nix") || os.contains("nux")) {
|
||||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (IOException e) {
|
||||||
log.error("Error opening browser: {}", e.getMessage());
|
log.error("Error opening browser: {}", e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,7 +166,17 @@ public class SPDFApplication {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Value("${server.port:8080}")
|
@Value("${server.port:8080}")
|
||||||
public void setServerPortStatic(String port) {
|
public void setServerPort(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setServerPortStatic(String port) {
|
||||||
if ("auto".equalsIgnoreCase(port)) {
|
if ("auto".equalsIgnoreCase(port)) {
|
||||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||||
SPDFApplication.serverPortStatic =
|
SPDFApplication.serverPortStatic =
|
||||||
@@ -195,36 +213,11 @@ public class SPDFApplication {
|
|||||||
return new String[] {"default"};
|
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() {
|
public static String getStaticBaseUrl() {
|
||||||
return baseUrlStatic;
|
return baseUrlStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNonStaticBaseUrl() {
|
|
||||||
return baseUrlStatic;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticPort() {
|
public static String getStaticPort() {
|
||||||
return serverPortStatic;
|
return serverPortStatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNonStaticPort() {
|
|
||||||
return serverPortStatic;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,11 +34,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import me.friwi.jcefmaven.CefAppBuilder;
|
import me.friwi.jcefmaven.CefAppBuilder;
|
||||||
import me.friwi.jcefmaven.EnumProgress;
|
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;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.utils.UIScaling;
|
import stirling.software.SPDF.utils.UIScaling;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import javax.swing.*;
|
|||||||
import io.github.pixee.security.BoundedLineReader;
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.UIScaling;
|
import stirling.software.SPDF.utils.UIScaling;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.springframework.core.io.ResourceLoader;
|
|||||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -34,10 +35,7 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||||
name = "system.customHTMLFiles",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||||
@@ -98,9 +96,9 @@ public class AppConfig {
|
|||||||
|
|
||||||
@Bean(name = "rateLimit")
|
@Bean(name = "rateLimit")
|
||||||
public boolean rateLimit() {
|
public boolean rateLimit() {
|
||||||
String appName = System.getProperty("rateLimit");
|
String rateLimit = System.getProperty("rateLimit");
|
||||||
if (appName == null) appName = System.getenv("rateLimit");
|
if (rateLimit == null) rateLimit = System.getenv("rateLimit");
|
||||||
return (appName != null) ? Boolean.valueOf(appName) : false;
|
return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "RunningInDocker")
|
@Bean(name = "RunningInDocker")
|
||||||
@@ -127,18 +125,9 @@ public class AppConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "bookAndHtmlFormatsInstalled")
|
|
||||||
public boolean bookAndHtmlFormatsInstalled() {
|
|
||||||
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
|
||||||
if (installOps == null) {
|
|
||||||
installOps = System.getenv("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
|
|
||||||
}
|
|
||||||
return "true".equalsIgnoreCase(installOps);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||||
@Bean(name = "activSecurity")
|
@Bean(name = "activeSecurity")
|
||||||
public boolean missingActivSecurity() {
|
public boolean missingActiveSecurity() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,16 +170,14 @@ public class AppConfig {
|
|||||||
@Bean(name = "analyticsPrompt")
|
@Bean(name = "analyticsPrompt")
|
||||||
@Scope("request")
|
@Scope("request")
|
||||||
public boolean analyticsPrompt() {
|
public boolean analyticsPrompt() {
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() == null
|
return applicationProperties.getSystem().getEnableAnalytics() == null;
|
||||||
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "analyticsEnabled")
|
@Bean(name = "analyticsEnabled")
|
||||||
@Scope("request")
|
@Scope("request")
|
||||||
public boolean analyticsEnabled() {
|
public boolean analyticsEnabled() {
|
||||||
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
||||||
return applicationProperties.getSystem().getEnableAnalytics() != null
|
return applicationProperties.getSystem().isAnalyticsEnabled();
|
||||||
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean(name = "StirlingPDFLabel")
|
@Bean(name = "StirlingPDFLabel")
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package stirling.software.SPDF.config;
|
|||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Configuration
|
||||||
class AppUpdateService {
|
class AppUpdateService {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
|||||||
"endpoints",
|
"endpoints",
|
||||||
"logout",
|
"logout",
|
||||||
"error",
|
"error",
|
||||||
"erroroauth",
|
"errorOAuth",
|
||||||
"file",
|
"file",
|
||||||
"messageType",
|
"messageType",
|
||||||
"infoMessage");
|
"infoMessage");
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ public class ConfigInitializer {
|
|||||||
log.info("Created settings file from template");
|
log.info("Created settings file from template");
|
||||||
} else {
|
} else {
|
||||||
// 2) Merge existing file with the template
|
// 2) Merge existing file with the template
|
||||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
|
||||||
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
|
URL templateResource = getClass().getClassLoader().getResource("settings.yml.template");
|
||||||
if (templateResource == null) {
|
if (templateResource == null) {
|
||||||
throw new IOException("Resource not found: settings.yml.template");
|
throw new IOException("Resource not found: settings.yml.template");
|
||||||
@@ -49,160 +47,33 @@ public class ConfigInitializer {
|
|||||||
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2a) Read lines from both files
|
// Copy setting.yaml to a temp location so we can read lines
|
||||||
List<String> templateLines = Files.readAllLines(tempTemplatePath);
|
Path settingTempPath = Files.createTempFile("settings", ".yaml");
|
||||||
List<String> mainLines = Files.readAllLines(settingsPath);
|
try (InputStream in = Files.newInputStream(destPath)) {
|
||||||
|
Files.copy(in, settingTempPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
|
||||||
// 2b) Merge lines
|
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
|
||||||
List<String> mergedLines = mergeYamlLinesWithTemplate(templateLines, mainLines);
|
YamlHelper settingsFile = new YamlHelper(settingTempPath);
|
||||||
|
|
||||||
// 2c) Only write if there's an actual difference
|
boolean changesMade =
|
||||||
if (!mergedLines.equals(mainLines)) {
|
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
|
||||||
Files.write(settingsPath, mergedLines);
|
if (changesMade) {
|
||||||
|
settingsTemplateFile.save(destPath);
|
||||||
log.info("Settings file updated based on template changes.");
|
log.info("Settings file updated based on template changes.");
|
||||||
} else {
|
} else {
|
||||||
log.info("No changes detected; settings file left as-is.");
|
log.info("No changes detected; settings file left as-is.");
|
||||||
}
|
}
|
||||||
|
|
||||||
Files.deleteIfExists(tempTemplatePath);
|
Files.deleteIfExists(tempTemplatePath);
|
||||||
|
Files.deleteIfExists(settingTempPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Ensure custom settings file exists
|
// 3) Ensure custom settings file exists
|
||||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||||
if (!Files.exists(customSettingsPath)) {
|
if (Files.notExists(customSettingsPath)) {
|
||||||
Files.createFile(customSettingsPath);
|
Files.createFile(customSettingsPath);
|
||||||
|
log.info("Created custom_settings file: {}", customSettingsPath.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge logic that: - Reads the template lines block-by-block (where a "block" = a key and all
|
|
||||||
* the lines that belong to it), - If the main file has that key, we keep the main file's block
|
|
||||||
* (preserving whitespace + inline comments). - Otherwise, we insert the template's block. - We
|
|
||||||
* also remove keys from main that no longer exist in the template.
|
|
||||||
*
|
|
||||||
* @param templateLines lines from settings.yml.template
|
|
||||||
* @param mainLines lines from the existing settings.yml
|
|
||||||
* @return merged lines
|
|
||||||
*/
|
|
||||||
private List<String> mergeYamlLinesWithTemplate(
|
|
||||||
List<String> templateLines, List<String> mainLines) {
|
|
||||||
|
|
||||||
// 1) Parse template lines into an ordered map: path -> Block
|
|
||||||
LinkedHashMap<String, Block> templateBlocks = parseYamlBlocks(templateLines);
|
|
||||||
|
|
||||||
// 2) Parse main lines into a map: path -> Block
|
|
||||||
LinkedHashMap<String, Block> mainBlocks = parseYamlBlocks(mainLines);
|
|
||||||
|
|
||||||
// 3) Build the final list by iterating template blocks in order
|
|
||||||
List<String> merged = new ArrayList<>();
|
|
||||||
for (Map.Entry<String, Block> entry : templateBlocks.entrySet()) {
|
|
||||||
String path = entry.getKey();
|
|
||||||
Block templateBlock = entry.getValue();
|
|
||||||
|
|
||||||
if (mainBlocks.containsKey(path)) {
|
|
||||||
// If main has the same block, prefer main's lines
|
|
||||||
merged.addAll(mainBlocks.get(path).lines);
|
|
||||||
} else {
|
|
||||||
// Otherwise, add the template block
|
|
||||||
merged.addAll(templateBlock.lines);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a list of lines into a map of "path -> Block" where "Block" is all lines that belong to
|
|
||||||
* that key (including subsequent indented lines). Very naive approach that may not work with
|
|
||||||
* advanced YAML.
|
|
||||||
*/
|
|
||||||
private LinkedHashMap<String, Block> parseYamlBlocks(List<String> lines) {
|
|
||||||
LinkedHashMap<String, Block> blocks = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
Block currentBlock = null;
|
|
||||||
String currentPath = null;
|
|
||||||
|
|
||||||
for (String line : lines) {
|
|
||||||
if (isLikelyKeyLine(line)) {
|
|
||||||
// Found a new "key: ..." line
|
|
||||||
if (currentBlock != null && currentPath != null) {
|
|
||||||
blocks.put(currentPath, currentBlock);
|
|
||||||
}
|
|
||||||
currentBlock = new Block();
|
|
||||||
currentBlock.lines.add(line);
|
|
||||||
currentPath = computePathForLine(line);
|
|
||||||
} else {
|
|
||||||
// Continuation of current block (comments, blank lines, sub-lines)
|
|
||||||
if (currentBlock == null) {
|
|
||||||
// If file starts with comments/blank lines, treat as "header block" with path
|
|
||||||
// ""
|
|
||||||
currentBlock = new Block();
|
|
||||||
currentPath = "";
|
|
||||||
}
|
|
||||||
currentBlock.lines.add(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentBlock != null && currentPath != null) {
|
|
||||||
blocks.put(currentPath, currentBlock);
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the line is likely "key:" or "key: value", ignoring comments/blank. Skips lines
|
|
||||||
* starting with "-" or "#".
|
|
||||||
*/
|
|
||||||
private boolean isLikelyKeyLine(String line) {
|
|
||||||
String trimmed = line.trim();
|
|
||||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int colonIdx = trimmed.indexOf(':');
|
|
||||||
return (colonIdx > 0); // someKey:
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a line like "security: ", returns "security" or "security.enableLogin"
|
|
||||||
// by looking at indentation. Very naive.
|
|
||||||
private static final Deque<String> pathStack = new ArrayDeque<>();
|
|
||||||
private static int currentIndentLevel = 0;
|
|
||||||
|
|
||||||
private String computePathForLine(String line) {
|
|
||||||
// count leading spaces
|
|
||||||
int leadingSpaces = 0;
|
|
||||||
for (char c : line.toCharArray()) {
|
|
||||||
if (c == ' ') leadingSpaces++;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
// assume 2 spaces = 1 indent
|
|
||||||
int indentLevel = leadingSpaces / 2;
|
|
||||||
|
|
||||||
String trimmed = line.trim();
|
|
||||||
int colonIdx = trimmed.indexOf(':');
|
|
||||||
String keyName = trimmed.substring(0, colonIdx).trim();
|
|
||||||
|
|
||||||
// pop stack until we match the new indent level
|
|
||||||
while (currentIndentLevel >= indentLevel && !pathStack.isEmpty()) {
|
|
||||||
pathStack.pop();
|
|
||||||
currentIndentLevel--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// push the new key
|
|
||||||
pathStack.push(keyName);
|
|
||||||
currentIndentLevel = indentLevel;
|
|
||||||
|
|
||||||
// build path by reversing the stack
|
|
||||||
String[] arr = pathStack.toArray(new String[0]);
|
|
||||||
List<String> reversed = Arrays.asList(arr);
|
|
||||||
Collections.reverse(reversed);
|
|
||||||
return String.join(".", reversed);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple holder for the lines that comprise a "block" (i.e. a key and its subsequent lines).
|
|
||||||
*/
|
|
||||||
private static class Block {
|
|
||||||
List<String> lines = new ArrayList<>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -9,30 +8,24 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.context.annotation.DependsOn;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@DependsOn({"bookAndHtmlFormatsInstalled"})
|
|
||||||
public class EndpointConfiguration {
|
public class EndpointConfiguration {
|
||||||
|
|
||||||
private static final String REMOVE_BLANKS = "remove-blanks";
|
private static final String REMOVE_BLANKS = "remove-blanks";
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
|
||||||
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
|
||||||
private boolean bookAndHtmlFormatsInstalled;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public EndpointConfiguration(
|
public EndpointConfiguration(ApplicationProperties applicationProperties) {
|
||||||
ApplicationProperties applicationProperties,
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
init();
|
init();
|
||||||
processEnvironmentConfigs();
|
processEnvironmentConfigs();
|
||||||
}
|
}
|
||||||
@@ -197,8 +190,8 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
addEndpointToGroup("LibreOffice", "pdf-to-html");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
addEndpointToGroup("LibreOffice", "pdf-to-xml");
|
||||||
|
|
||||||
// Unoconv
|
// Unoconvert
|
||||||
addEndpointToGroup("Unoconv", "file-to-pdf");
|
addEndpointToGroup("Unoconvert", "file-to-pdf");
|
||||||
|
|
||||||
// qpdf
|
// qpdf
|
||||||
addEndpointToGroup("qpdf", "compress-pdf");
|
addEndpointToGroup("qpdf", "compress-pdf");
|
||||||
@@ -272,12 +265,6 @@ public class EndpointConfiguration {
|
|||||||
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 (groupsToRemove == null) {
|
|
||||||
groupsToRemove = new ArrayList<>();
|
|
||||||
}
|
|
||||||
groupsToRemove.add("Calibre");
|
|
||||||
}
|
|
||||||
if (endpointsToRemove != null) {
|
if (endpointsToRemove != null) {
|
||||||
for (String endpoint : endpointsToRemove) {
|
for (String endpoint : endpointsToRemove) {
|
||||||
disableEndpoint(endpoint.trim());
|
disableEndpoint(endpoint.trim());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import java.util.stream.Collectors;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -16,21 +17,29 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class ExternalAppDepConfig {
|
public class ExternalAppDepConfig {
|
||||||
|
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
private final Map<String, List<String>> commandToGroupMapping =
|
|
||||||
new HashMap<>() {
|
|
||||||
|
|
||||||
{
|
private final String weasyprintPath;
|
||||||
put("soffice", List.of("LibreOffice"));
|
private final String unoconvPath;
|
||||||
put("/opt/venv/bin/weasyprint", List.of("Weasyprint"));
|
private final Map<String, List<String>> commandToGroupMapping;
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
|
||||||
put("/opt/venv/bin/unoconvert", List.of("Unoconv"));
|
|
||||||
put("qpdf", List.of("qpdf"));
|
|
||||||
put("tesseract", List.of("tesseract"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public ExternalAppDepConfig(EndpointConfiguration endpointConfiguration) {
|
public ExternalAppDepConfig(
|
||||||
|
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
|
||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
|
weasyprintPath = runtimePathConfig.getWeasyPrintPath();
|
||||||
|
unoconvPath = runtimePathConfig.getUnoConvertPath();
|
||||||
|
|
||||||
|
commandToGroupMapping =
|
||||||
|
new HashMap<>() {
|
||||||
|
|
||||||
|
{
|
||||||
|
put("soffice", List.of("LibreOffice"));
|
||||||
|
put(weasyprintPath, List.of("Weasyprint"));
|
||||||
|
put("pdftohtml", List.of("Pdftohtml"));
|
||||||
|
put(unoconvPath, List.of("Unoconvert"));
|
||||||
|
put("qpdf", List.of("qpdf"));
|
||||||
|
put("tesseract", List.of("tesseract"));
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCommandAvailable(String command) {
|
private boolean isCommandAvailable(String command) {
|
||||||
@@ -101,9 +110,9 @@ public class ExternalAppDepConfig {
|
|||||||
checkDependencyAndDisableGroup("tesseract");
|
checkDependencyAndDisableGroup("tesseract");
|
||||||
checkDependencyAndDisableGroup("soffice");
|
checkDependencyAndDisableGroup("soffice");
|
||||||
checkDependencyAndDisableGroup("qpdf");
|
checkDependencyAndDisableGroup("qpdf");
|
||||||
checkDependencyAndDisableGroup("/opt/venv/bin/weasyprint");
|
checkDependencyAndDisableGroup(weasyprintPath);
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup("/opt/venv/bin/unoconvert");
|
checkDependencyAndDisableGroup(unoconvPath);
|
||||||
// Special handling for Python/OpenCV dependencies
|
// Special handling for Python/OpenCV dependencies
|
||||||
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
|
||||||
if (!pythonAvailable) {
|
if (!pythonAvailable) {
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import org.springframework.stereotype.Component;
|
|||||||
import io.micrometer.common.util.StringUtils;
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@@ -42,7 +44,7 @@ public class InitialSetup {
|
|||||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||||
// Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
uuid = UUID.randomUUID().toString();
|
uuid = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.UUID", uuid);
|
||||||
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ public class InitialSetup {
|
|||||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
if (!GeneralUtils.isValidUUID(secretKey)) {
|
||||||
// Generating a random UUID as the secret key
|
// Generating a random UUID as the secret key
|
||||||
secretKey = UUID.randomUUID().toString();
|
secretKey = UUID.randomUUID().toString();
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.key", secretKey);
|
||||||
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,8 +64,8 @@ public class InitialSetup {
|
|||||||
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
|
||||||
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
|
||||||
if (!csrf) {
|
if (!csrf) {
|
||||||
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
|
GeneralUtils.saveKeyToSettings("security.csrfDisabled", false);
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
|
GeneralUtils.saveKeyToSettings("system.enableAnalytics", true);
|
||||||
applicationProperties.getSecurity().setCsrfDisabled(false);
|
applicationProperties.getSecurity().setCsrfDisabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,14 +76,14 @@ public class InitialSetup {
|
|||||||
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
||||||
if (StringUtils.isEmpty(termsUrl)) {
|
if (StringUtils.isEmpty(termsUrl)) {
|
||||||
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
||||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
|
GeneralUtils.saveKeyToSettings("legal.termsAndConditions", defaultTermsUrl);
|
||||||
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
||||||
}
|
}
|
||||||
// Initialize Privacy Policy
|
// Initialize Privacy Policy
|
||||||
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
||||||
if (StringUtils.isEmpty(privacyUrl)) {
|
if (StringUtils.isEmpty(privacyUrl)) {
|
||||||
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
||||||
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
|
GeneralUtils.saveKeyToSettings("legal.privacyPolicy", defaultPrivacyUrl);
|
||||||
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +97,7 @@ public class InitialSetup {
|
|||||||
appVersion = props.getProperty("version");
|
appVersion = props.getProperty("version");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
}
|
}
|
||||||
|
GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion);
|
||||||
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
|
||||||
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -11,7 +12,6 @@ public class InstallationPathConfig {
|
|||||||
// Root paths
|
// Root paths
|
||||||
private static final String LOG_PATH;
|
private static final String LOG_PATH;
|
||||||
private static final String CONFIG_PATH;
|
private static final String CONFIG_PATH;
|
||||||
private static final String PIPELINE_PATH;
|
|
||||||
private static final String CUSTOM_FILES_PATH;
|
private static final String CUSTOM_FILES_PATH;
|
||||||
private static final String CLIENT_WEBUI_PATH;
|
private static final String CLIENT_WEBUI_PATH;
|
||||||
|
|
||||||
@@ -19,11 +19,6 @@ public class InstallationPathConfig {
|
|||||||
private static final String SETTINGS_PATH;
|
private static final String SETTINGS_PATH;
|
||||||
private static final String CUSTOM_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;
|
|
||||||
private static final String PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
|
||||||
|
|
||||||
// Custom file paths
|
// Custom file paths
|
||||||
private static final String STATIC_PATH;
|
private static final String STATIC_PATH;
|
||||||
private static final String TEMPLATES_PATH;
|
private static final String TEMPLATES_PATH;
|
||||||
@@ -35,7 +30,6 @@ public class InstallationPathConfig {
|
|||||||
// Initialize root paths
|
// Initialize root paths
|
||||||
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
LOG_PATH = BASE_PATH + "logs" + File.separator;
|
||||||
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
|
||||||
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
|
|
||||||
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
|
||||||
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
|
||||||
|
|
||||||
@@ -43,11 +37,6 @@ public class InstallationPathConfig {
|
|||||||
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
|
||||||
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_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;
|
|
||||||
PIPELINE_DEFAULT_WEB_UI_CONFIGS = PIPELINE_PATH + "defaultWebUIConfigs" + File.separator;
|
|
||||||
|
|
||||||
// Initialize custom file paths
|
// Initialize custom file paths
|
||||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||||
@@ -58,26 +47,29 @@ public class InstallationPathConfig {
|
|||||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||||
String os = System.getProperty("os.name").toLowerCase();
|
String os = System.getProperty("os.name").toLowerCase();
|
||||||
if (os.contains("win")) {
|
if (os.contains("win")) {
|
||||||
return System.getenv("APPDATA") + File.separator + "Stirling-PDF" + File.separator;
|
return Paths.get(
|
||||||
|
System.getenv("APPDATA"), // parent path
|
||||||
|
"Stirling-PDF")
|
||||||
|
.toString()
|
||||||
|
+ File.separator;
|
||||||
} else if (os.contains("mac")) {
|
} else if (os.contains("mac")) {
|
||||||
return System.getProperty("user.home")
|
return Paths.get(
|
||||||
+ File.separator
|
System.getProperty("user.home"),
|
||||||
+ "Library"
|
"Library",
|
||||||
+ File.separator
|
"Application Support",
|
||||||
+ "Application Support"
|
"Stirling-PDF")
|
||||||
+ File.separator
|
.toString()
|
||||||
+ "Stirling-PDF"
|
|
||||||
+ File.separator;
|
+ File.separator;
|
||||||
} else {
|
} else {
|
||||||
return System.getProperty("user.home")
|
return Paths.get(
|
||||||
+ File.separator
|
System.getProperty("user.home"), // parent path
|
||||||
+ ".config"
|
".config",
|
||||||
+ File.separator
|
"Stirling-PDF")
|
||||||
+ "Stirling-PDF"
|
.toString()
|
||||||
+ File.separator;
|
+ File.separator;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "./";
|
return "." + File.separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPath() {
|
public static String getPath() {
|
||||||
@@ -92,10 +84,6 @@ public class InstallationPathConfig {
|
|||||||
return CONFIG_PATH;
|
return CONFIG_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getPipelinePath() {
|
|
||||||
return PIPELINE_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getCustomFilesPath() {
|
public static String getCustomFilesPath() {
|
||||||
return CUSTOM_FILES_PATH;
|
return CUSTOM_FILES_PATH;
|
||||||
}
|
}
|
||||||
@@ -112,18 +100,6 @@ public class InstallationPathConfig {
|
|||||||
return CUSTOM_SETTINGS_PATH;
|
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 getPipelineDefaultWebUIConfigsDir() {
|
|
||||||
return PIPELINE_DEFAULT_WEB_UI_CONFIGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getStaticPath() {
|
public static String getStaticPath() {
|
||||||
return STATIC_PATH;
|
return STATIC_PATH;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import com.posthog.java.PostHog;
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
import jakarta.annotation.PreDestroy;
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
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.ApplicationProperties.CustomPaths.Operations;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties.CustomPaths.Pipeline;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
@Getter
|
||||||
|
public class RuntimePathConfig {
|
||||||
|
private final ApplicationProperties properties;
|
||||||
|
private final String basePath;
|
||||||
|
private final String weasyPrintPath;
|
||||||
|
private final String unoConvertPath;
|
||||||
|
|
||||||
|
// Pipeline paths
|
||||||
|
private final String pipelineWatchedFoldersPath;
|
||||||
|
private final String pipelineFinishedFoldersPath;
|
||||||
|
private final String pipelineDefaultWebUiConfigs;
|
||||||
|
private final String pipelinePath;
|
||||||
|
|
||||||
|
public RuntimePathConfig(ApplicationProperties properties) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.basePath = InstallationPathConfig.getPath();
|
||||||
|
|
||||||
|
this.pipelinePath = Path.of(basePath, "pipeline").toString();
|
||||||
|
String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
|
||||||
|
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||||
|
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||||
|
|
||||||
|
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
|
||||||
|
|
||||||
|
this.pipelineWatchedFoldersPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultWatchedFolders,
|
||||||
|
pipeline != null ? pipeline.getWatchedFoldersDir() : null);
|
||||||
|
this.pipelineFinishedFoldersPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultFinishedFolders,
|
||||||
|
pipeline != null ? pipeline.getFinishedFoldersDir() : null);
|
||||||
|
this.pipelineDefaultWebUiConfigs =
|
||||||
|
resolvePath(
|
||||||
|
defaultWebUIConfigs,
|
||||||
|
pipeline != null ? pipeline.getWebUIConfigsDir() : null);
|
||||||
|
|
||||||
|
boolean isDocker = isRunningInDocker();
|
||||||
|
|
||||||
|
// Initialize Operation paths
|
||||||
|
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||||
|
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||||
|
|
||||||
|
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
||||||
|
this.weasyPrintPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultWeasyPrintPath,
|
||||||
|
operations != null ? operations.getWeasyprint() : null);
|
||||||
|
this.unoConvertPath =
|
||||||
|
resolvePath(
|
||||||
|
defaultUnoConvertPath,
|
||||||
|
operations != null ? operations.getUnoconvert() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolvePath(String defaultPath, String customPath) {
|
||||||
|
return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunningInDocker() {
|
||||||
|
return Files.exists(Path.of("/.dockerenv"));
|
||||||
|
}
|
||||||
|
}
|
||||||
479
src/main/java/stirling/software/SPDF/config/YamlHelper.java
Normal file
479
src/main/java/stirling/software/SPDF/config/YamlHelper.java
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.snakeyaml.engine.v2.api.Dump;
|
||||||
|
import org.snakeyaml.engine.v2.api.DumpSettings;
|
||||||
|
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||||
|
import org.snakeyaml.engine.v2.api.StreamDataWriter;
|
||||||
|
import org.snakeyaml.engine.v2.common.FlowStyle;
|
||||||
|
import org.snakeyaml.engine.v2.common.ScalarStyle;
|
||||||
|
import org.snakeyaml.engine.v2.composer.Composer;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.MappingNode;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.Node;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.NodeTuple;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.ScalarNode;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.SequenceNode;
|
||||||
|
import org.snakeyaml.engine.v2.nodes.Tag;
|
||||||
|
import org.snakeyaml.engine.v2.parser.ParserImpl;
|
||||||
|
import org.snakeyaml.engine.v2.scanner.StreamReader;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class YamlHelper {
|
||||||
|
|
||||||
|
// YAML dump settings with comment support and block flow style
|
||||||
|
private static final DumpSettings DUMP_SETTINGS =
|
||||||
|
DumpSettings.builder()
|
||||||
|
.setDumpComments(true)
|
||||||
|
.setWidth(Integer.MAX_VALUE)
|
||||||
|
.setDefaultFlowStyle(FlowStyle.BLOCK)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final String yamlContent; // Stores the entire YAML content as a string
|
||||||
|
|
||||||
|
private LoadSettings loadSettings =
|
||||||
|
LoadSettings.builder()
|
||||||
|
.setUseMarks(true)
|
||||||
|
.setMaxAliasesForCollections(Integer.MAX_VALUE)
|
||||||
|
.setAllowRecursiveKeys(true)
|
||||||
|
.setParseComments(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private Path originalFilePath;
|
||||||
|
private Node updatedRootNode;
|
||||||
|
|
||||||
|
// Constructor with custom LoadSettings and YAML string
|
||||||
|
public YamlHelper(LoadSettings loadSettings, String yamlContent) {
|
||||||
|
this.loadSettings = loadSettings;
|
||||||
|
this.yamlContent = yamlContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor that reads YAML from a file path
|
||||||
|
public YamlHelper(Path originalFilePath) throws IOException {
|
||||||
|
this.yamlContent = Files.readString(originalFilePath);
|
||||||
|
this.originalFilePath = originalFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates values in the target YAML based on values from the source YAML. It ensures that only
|
||||||
|
* existing keys in the target YAML are updated.
|
||||||
|
*
|
||||||
|
* @return true if at least one key was updated, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean updateValuesFromYaml(YamlHelper sourceYaml, YamlHelper targetYaml) {
|
||||||
|
boolean updated = false;
|
||||||
|
Set<String> sourceKeys = sourceYaml.getAllKeys();
|
||||||
|
Set<String> targetKeys = targetYaml.getAllKeys();
|
||||||
|
|
||||||
|
for (String key : sourceKeys) {
|
||||||
|
String[] keyArray = key.split("\\.");
|
||||||
|
|
||||||
|
Object newValue = sourceYaml.getValueByExactKeyPath(keyArray);
|
||||||
|
Object currentValue = targetYaml.getValueByExactKeyPath(keyArray);
|
||||||
|
if (newValue != null
|
||||||
|
&& (!newValue.equals(currentValue) || !sourceKeys.equals(targetKeys))) {
|
||||||
|
boolean updatedKey = targetYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||||
|
if (updatedKey) updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a value in the YAML structure.
|
||||||
|
*
|
||||||
|
* @param keys The hierarchical keys leading to the value.
|
||||||
|
* @param newValue The new value to set.
|
||||||
|
* @return true if the value was updated, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean updateValue(List<String> keys, Object newValue) {
|
||||||
|
return updateValue(getRootNode(), keys, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean updateValue(Node node, List<String> keys, Object newValue) {
|
||||||
|
if (!(node instanceof MappingNode mappingNode)) return false;
|
||||||
|
|
||||||
|
List<NodeTuple> updatedTuples = new ArrayList<>();
|
||||||
|
boolean updated = false;
|
||||||
|
|
||||||
|
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||||
|
ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null;
|
||||||
|
if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) {
|
||||||
|
updatedTuples.add(tuple);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node valueNode = tuple.getValueNode();
|
||||||
|
|
||||||
|
if (keys.size() == 1) {
|
||||||
|
Tag tag = valueNode.getTag();
|
||||||
|
Node newValueNode = null;
|
||||||
|
|
||||||
|
if (isAnyInteger(newValue)) {
|
||||||
|
newValueNode =
|
||||||
|
new ScalarNode(Tag.INT, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||||
|
} else if (isFloat(newValue)) {
|
||||||
|
Object floatValue = Float.valueOf(String.valueOf(newValue));
|
||||||
|
newValueNode =
|
||||||
|
new ScalarNode(
|
||||||
|
Tag.FLOAT, String.valueOf(floatValue), ScalarStyle.PLAIN);
|
||||||
|
} else if ("true".equals(newValue) || "false".equals(newValue)) {
|
||||||
|
newValueNode =
|
||||||
|
new ScalarNode(Tag.BOOL, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||||
|
} else if (newValue instanceof List<?> list) {
|
||||||
|
List<Node> sequenceNodes = new ArrayList<>();
|
||||||
|
for (Object item : list) {
|
||||||
|
Object obj = String.valueOf(item);
|
||||||
|
if (isAnyInteger(item)) {
|
||||||
|
tag = Tag.INT;
|
||||||
|
} else if (isFloat(item)) {
|
||||||
|
obj = Float.valueOf(String.valueOf(item));
|
||||||
|
tag = Tag.FLOAT;
|
||||||
|
} else if ("true".equals(item) || "false".equals(item)) {
|
||||||
|
tag = Tag.BOOL;
|
||||||
|
} else if (item == null || "null".equals(item)) {
|
||||||
|
tag = Tag.NULL;
|
||||||
|
} else {
|
||||||
|
tag = Tag.STR;
|
||||||
|
}
|
||||||
|
sequenceNodes.add(
|
||||||
|
new ScalarNode(tag, String.valueOf(obj), ScalarStyle.PLAIN));
|
||||||
|
}
|
||||||
|
newValueNode = new SequenceNode(Tag.SEQ, sequenceNodes, FlowStyle.FLOW);
|
||||||
|
} else if (tag == Tag.NULL) {
|
||||||
|
if ("true".equals(newValue)
|
||||||
|
|| "false".equals(newValue)
|
||||||
|
|| newValue instanceof Boolean) {
|
||||||
|
tag = Tag.BOOL;
|
||||||
|
}
|
||||||
|
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||||
|
} else {
|
||||||
|
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||||
|
}
|
||||||
|
copyComments(valueNode, newValueNode);
|
||||||
|
|
||||||
|
updatedTuples.add(new NodeTuple(keyNode, newValueNode));
|
||||||
|
updated = true;
|
||||||
|
} else if (valueNode instanceof MappingNode) {
|
||||||
|
updated = updateValue(valueNode, keys.subList(1, keys.size()), newValue);
|
||||||
|
updatedTuples.add(tuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
mappingNode.getValue().clear();
|
||||||
|
mappingNode.getValue().addAll(updatedTuples);
|
||||||
|
}
|
||||||
|
setNewNode(node);
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a value based on an exact key path.
|
||||||
|
*
|
||||||
|
* @param keys The key hierarchy leading to the value.
|
||||||
|
* @return The value if found, otherwise null.
|
||||||
|
*/
|
||||||
|
public Object getValueByExactKeyPath(String... keys) {
|
||||||
|
return getValueByExactKeyPath(getRootNode(), new ArrayDeque<>(List.of(keys)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getValueByExactKeyPath(Node node, Deque<String> keyQueue) {
|
||||||
|
if (!(node instanceof MappingNode mappingNode)) return null;
|
||||||
|
|
||||||
|
String currentKey = keyQueue.poll();
|
||||||
|
if (currentKey == null) return null;
|
||||||
|
|
||||||
|
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||||
|
if (tuple.getKeyNode() instanceof ScalarNode keyNode
|
||||||
|
&& keyNode.getValue().equals(currentKey)) {
|
||||||
|
if (keyQueue.isEmpty()) {
|
||||||
|
Node valueNode = tuple.getValueNode();
|
||||||
|
|
||||||
|
if (valueNode instanceof ScalarNode scalarValueNode) {
|
||||||
|
return scalarValueNode.getValue();
|
||||||
|
} else if (valueNode instanceof MappingNode subMapping) {
|
||||||
|
return getValueByExactKeyPath(subMapping, keyQueue);
|
||||||
|
} else if (valueNode instanceof SequenceNode sequenceNode) {
|
||||||
|
List<Object> valuesList = new ArrayList<>();
|
||||||
|
for (Node o : sequenceNode.getValue()) {
|
||||||
|
if (o instanceof ScalarNode scalarValue) {
|
||||||
|
valuesList.add(scalarValue.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valuesList;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getValueByExactKeyPath(tuple.getValueNode(), keyQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> cachedKeys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the set of all keys present in the YAML structure. Keys are returned as
|
||||||
|
* dot-separated paths for nested keys.
|
||||||
|
*
|
||||||
|
* @return A set containing all keys in dot notation.
|
||||||
|
*/
|
||||||
|
public Set<String> getAllKeys() {
|
||||||
|
if (cachedKeys == null) {
|
||||||
|
cachedKeys = getAllKeys(getRootNode());
|
||||||
|
}
|
||||||
|
return cachedKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects all keys from the YAML node recursively.
|
||||||
|
*
|
||||||
|
* @param node The current YAML node.
|
||||||
|
* @param currentPath The accumulated path of keys.
|
||||||
|
* @param allKeys The set storing all collected keys.
|
||||||
|
*/
|
||||||
|
private Set<String> getAllKeys(Node node) {
|
||||||
|
Set<String> allKeys = new LinkedHashSet<>();
|
||||||
|
collectKeys(node, "", allKeys);
|
||||||
|
return allKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively traverses the YAML structure to collect all keys.
|
||||||
|
*
|
||||||
|
* @param node The current node in the YAML structure.
|
||||||
|
* @param currentPath The accumulated key path.
|
||||||
|
* @param allKeys The set storing collected keys.
|
||||||
|
*/
|
||||||
|
private void collectKeys(Node node, String currentPath, Set<String> allKeys) {
|
||||||
|
if (node instanceof MappingNode mappingNode) {
|
||||||
|
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||||
|
if (tuple.getKeyNode() instanceof ScalarNode keyNode) {
|
||||||
|
String newPath =
|
||||||
|
currentPath.isEmpty()
|
||||||
|
? keyNode.getValue()
|
||||||
|
: currentPath + "." + keyNode.getValue();
|
||||||
|
allKeys.add(newPath);
|
||||||
|
collectKeys(tuple.getValueNode(), newPath, allKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the root node of the YAML document. If a new node was previously set, it is
|
||||||
|
* returned instead.
|
||||||
|
*
|
||||||
|
* @return The root node of the YAML structure.
|
||||||
|
*/
|
||||||
|
private Node getRootNode() {
|
||||||
|
if (this.updatedRootNode != null) {
|
||||||
|
return this.updatedRootNode;
|
||||||
|
}
|
||||||
|
Composer composer = new Composer(loadSettings, getParserImpl());
|
||||||
|
Optional<Node> rootNodeOpt = composer.getSingleNode();
|
||||||
|
if (rootNodeOpt.isPresent()) {
|
||||||
|
return rootNodeOpt.get();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a new root node, allowing modifications to be tracked.
|
||||||
|
*
|
||||||
|
* @param newRootNode The modified root node.
|
||||||
|
*/
|
||||||
|
public void setNewNode(Node newRootNode) {
|
||||||
|
this.updatedRootNode = newRootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current root node (either the original or the updated one).
|
||||||
|
*
|
||||||
|
* @return The root node.
|
||||||
|
*/
|
||||||
|
public Node getUpdatedRootNode() {
|
||||||
|
if (this.updatedRootNode == null) {
|
||||||
|
this.updatedRootNode = getRootNode();
|
||||||
|
}
|
||||||
|
return this.updatedRootNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the YAML parser.
|
||||||
|
*
|
||||||
|
* @return The configured parser.
|
||||||
|
*/
|
||||||
|
private ParserImpl getParserImpl() {
|
||||||
|
return new ParserImpl(loadSettings, getStreamReader());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a stream reader for the YAML content.
|
||||||
|
*
|
||||||
|
* @return The configured stream reader.
|
||||||
|
*/
|
||||||
|
private StreamReader getStreamReader() {
|
||||||
|
return new StreamReader(loadSettings, yamlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MappingNode save(Path saveFilePath) throws IOException {
|
||||||
|
if (!saveFilePath.equals(originalFilePath)) {
|
||||||
|
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||||
|
}
|
||||||
|
return (MappingNode) getUpdatedRootNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveOverride(Path saveFilePath) throws IOException {
|
||||||
|
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a YAML node back to a YAML-formatted string.
|
||||||
|
*
|
||||||
|
* @param rootNode The root node to be converted.
|
||||||
|
* @return A YAML-formatted string.
|
||||||
|
*/
|
||||||
|
public String convertNodeToYaml(Node rootNode) {
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
StreamDataWriter streamDataWriter =
|
||||||
|
new StreamDataWriter() {
|
||||||
|
@Override
|
||||||
|
public void write(String str) {
|
||||||
|
writer.write(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(String str, int off, int len) {
|
||||||
|
writer.write(str, off, len);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
new Dump(DUMP_SETTINGS).dumpNode(rootNode, streamDataWriter);
|
||||||
|
return writer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isParsable(String value, Function<String, ?> parser) {
|
||||||
|
try {
|
||||||
|
parser.apply(value);
|
||||||
|
return true;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given object is an integer.
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents an integer, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||||
|
public static boolean isInteger(Object object) {
|
||||||
|
if (object instanceof Integer
|
||||||
|
|| object instanceof Short
|
||||||
|
|| object instanceof Byte
|
||||||
|
|| object instanceof Long) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (object instanceof String str) {
|
||||||
|
return isParsable(str, Integer::parseInt);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given object is a floating-point number.
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents a float, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||||
|
public static boolean isFloat(Object object) {
|
||||||
|
return (object instanceof Float || object instanceof Double)
|
||||||
|
|| (object instanceof String str && isParsable(str, Float::parseFloat));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given object is a short integer.
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents a short integer, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||||
|
public static boolean isShort(Object object) {
|
||||||
|
return (object instanceof Long)
|
||||||
|
|| (object instanceof String str && isParsable(str, Short::parseShort));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given object is a byte.
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents a byte, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||||
|
public static boolean isByte(Object object) {
|
||||||
|
return (object instanceof Long)
|
||||||
|
|| (object instanceof String str && isParsable(str, Byte::parseByte));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a given object is a long integer.
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents a long integer, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||||
|
public static boolean isLong(Object object) {
|
||||||
|
return (object instanceof Long)
|
||||||
|
|| (object instanceof String str && isParsable(str, Long::parseLong));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an object is any type of integer (short, byte, long, or int).
|
||||||
|
*
|
||||||
|
* @param object The object to check.
|
||||||
|
* @return True if the object represents an integer type, false otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean isAnyInteger(Object object) {
|
||||||
|
return isInteger(object) || isShort(object) || isByte(object) || isLong(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies comments from an old node to a new one.
|
||||||
|
*
|
||||||
|
* @param oldNode The original node with comments.
|
||||||
|
* @param newValueNode The new node to which comments should be copied.
|
||||||
|
*/
|
||||||
|
private void copyComments(Node oldNode, Node newValueNode) {
|
||||||
|
if (oldNode == null || newValueNode == null) return;
|
||||||
|
if (oldNode.getBlockComments() != null) {
|
||||||
|
newValueNode.setBlockComments(oldNode.getBlockComments());
|
||||||
|
}
|
||||||
|
if (oldNode.getInLineComments() != null) {
|
||||||
|
newValueNode.setInLineComments(oldNode.getInLineComments());
|
||||||
|
}
|
||||||
|
if (oldNode.getEndComments() != null) {
|
||||||
|
newValueNode.setEndComments(oldNode.getEndComments());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package stirling.software.SPDF.config.interfaces;
|
|||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
public interface DatabaseInterface {
|
public interface DatabaseInterface {
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -67,7 +69,7 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
|||||||
}
|
}
|
||||||
if (exception instanceof BadCredentialsException
|
if (exception instanceof BadCredentialsException
|
||||||
|| exception instanceof UsernameNotFoundException) {
|
|| exception instanceof UsernameNotFoundException) {
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof InternalAuthenticationServiceException
|
if (exception instanceof InternalAuthenticationServiceException
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -14,91 +14,75 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
|||||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
import com.coveo.saml.SamlClient;
|
import com.coveo.saml.SamlClient;
|
||||||
|
import com.coveo.saml.SamlException;
|
||||||
|
|
||||||
import jakarta.servlet.ServletException;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.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;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.Provider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|
||||||
import stirling.software.SPDF.utils.UrlUtils;
|
import stirling.software.SPDF.utils.UrlUtils;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
public static final String LOGOUT_PATH = "/login?logout=true";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLogoutSuccess(
|
public void onLogoutSuccess(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
throws IOException, ServletException {
|
throws IOException {
|
||||||
|
|
||||||
if (!response.isCommitted()) {
|
if (!response.isCommitted()) {
|
||||||
// Handle user logout due to disabled account
|
|
||||||
if (request.getParameter("userIsDisabled") != null) {
|
|
||||||
response.sendRedirect(
|
|
||||||
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle OAuth2 authentication error
|
|
||||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
|
||||||
response.sendRedirect(
|
|
||||||
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
// Handle SAML2 logout redirection
|
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||||
if (authentication instanceof Saml2Authentication) {
|
// Handle SAML2 logout redirection
|
||||||
getRedirect_saml2(request, response, authentication);
|
getRedirect_saml2(request, response, samlAuthentication);
|
||||||
return;
|
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
||||||
}
|
// Handle OAuth2 logout redirection
|
||||||
// Handle OAuth2 logout redirection
|
getRedirect_oauth2(request, response, oAuthToken);
|
||||||
else if (authentication instanceof OAuth2AuthenticationToken) {
|
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||||
getRedirect_oauth2(request, response, authentication);
|
// Handle Username/Password logout
|
||||||
return;
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
}
|
} else {
|
||||||
// Handle Username/Password logout
|
// Handle unknown authentication types
|
||||||
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Handle unknown authentication types
|
|
||||||
else {
|
|
||||||
log.error(
|
log.error(
|
||||||
"authentication class unknown: "
|
"Authentication class unknown: {}",
|
||||||
+ authentication.getClass().getSimpleName());
|
authentication.getClass().getSimpleName());
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Redirect to login page after logout
|
// Redirect to login page after logout
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
String path = checkForErrors(request);
|
||||||
return;
|
getRedirectStrategy().sendRedirect(request, response, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect for SAML2 authentication logout
|
// Redirect for SAML2 authentication logout
|
||||||
private void getRedirect_saml2(
|
private void getRedirect_saml2(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Saml2Authentication samlAuthentication)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||||
String registrationId = samlConf.getRegistrationId();
|
String registrationId = samlConf.getRegistrationId();
|
||||||
|
|
||||||
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
|
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
CustomSaml2AuthenticatedPrincipal principal =
|
||||||
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
|
||||||
|
|
||||||
String nameIdValue = principal.getName();
|
String nameIdValue = principal.name();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Read certificate from the resource
|
// Read certificate from the resource
|
||||||
@@ -109,27 +93,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
certificates.add(certificate);
|
certificates.add(certificate);
|
||||||
|
|
||||||
// Construct URLs required for SAML configuration
|
// Construct URLs required for SAML configuration
|
||||||
String serverUrl =
|
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
|
||||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
|
||||||
|
|
||||||
String relyingPartyIdentifier =
|
|
||||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
|
||||||
|
|
||||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
|
||||||
|
|
||||||
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
|
||||||
|
|
||||||
String idpIssuer = samlConf.getIdpIssuer();
|
|
||||||
|
|
||||||
// Create SamlClient instance for SAML logout
|
|
||||||
SamlClient samlClient =
|
|
||||||
new SamlClient(
|
|
||||||
relyingPartyIdentifier,
|
|
||||||
assertionConsumerServiceUrl,
|
|
||||||
idpUrl,
|
|
||||||
idpIssuer,
|
|
||||||
certificates,
|
|
||||||
SamlClient.SamlIdpBinding.POST);
|
|
||||||
|
|
||||||
// Read private key for service provider
|
// Read private key for service provider
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||||
@@ -141,96 +105,134 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
|||||||
// Redirect to identity provider for logout
|
// Redirect to identity provider for logout
|
||||||
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error(nameIdValue, e);
|
log.error(
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
"Error retrieving logout URL from Provider {} for user {}",
|
||||||
|
samlConf.getProvider(),
|
||||||
|
nameIdValue,
|
||||||
|
e);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect for OAuth2 authentication logout
|
// Redirect for OAuth2 authentication logout
|
||||||
private void getRedirect_oauth2(
|
private void getRedirect_oauth2(
|
||||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
OAuth2AuthenticationToken oAuthToken)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String param = "logout=true";
|
String registrationId;
|
||||||
String registrationId = null;
|
|
||||||
String issuer = null;
|
|
||||||
String clientId = null;
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
String path = checkForErrors(request);
|
||||||
|
|
||||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
||||||
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
|
||||||
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get OAuth2 provider details from configuration
|
|
||||||
Provider provider = oauth.getClient().get(registrationId);
|
|
||||||
issuer = provider.getIssuer();
|
|
||||||
clientId = provider.getClientId();
|
|
||||||
} catch (UnsupportedProviderException e) {
|
|
||||||
log.error(e.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
|
||||||
issuer = oauth.getIssuer();
|
|
||||||
clientId = oauth.getClientId();
|
|
||||||
}
|
|
||||||
String errorMessage = "";
|
|
||||||
// Handle different error scenarios during logout
|
|
||||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
|
||||||
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
|
||||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
|
||||||
param = "error=" + sanitizeInput(errorMessage);
|
|
||||||
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
|
||||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
|
||||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
|
||||||
param = "error=oauth2AutoCreateDisabled";
|
|
||||||
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
|
||||||
param = "erroroauth=oauth2_admin_blocked_user";
|
|
||||||
} else if (request.getParameter("userIsDisabled") != null) {
|
|
||||||
param = "erroroauth=userIsDisabled";
|
|
||||||
} else if (request.getParameter("badcredentials") != null) {
|
|
||||||
param = "error=badcredentials";
|
|
||||||
}
|
|
||||||
|
|
||||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
|
||||||
|
|
||||||
// Redirect based on OAuth2 provider
|
// Redirect based on OAuth2 provider
|
||||||
switch (registrationId.toLowerCase()) {
|
switch (registrationId.toLowerCase()) {
|
||||||
case "keycloak":
|
case "keycloak" -> {
|
||||||
// Add Keycloak specific logout URL if needed
|
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
|
||||||
String logoutUrl =
|
|
||||||
issuer
|
boolean isKeycloak = !keycloak.getIssuer().isBlank();
|
||||||
+ "/protocol/openid-connect/logout"
|
boolean isCustomOAuth = !oauth.getIssuer().isBlank();
|
||||||
+ "?client_id="
|
|
||||||
+ clientId
|
String logoutUrl = redirectUrl;
|
||||||
+ "&post_logout_redirect_uri="
|
|
||||||
+ response.encodeRedirectURL(redirect_url);
|
if (isKeycloak) {
|
||||||
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
logoutUrl = keycloak.getIssuer();
|
||||||
|
} else if (isCustomOAuth) {
|
||||||
|
logoutUrl = oauth.getIssuer();
|
||||||
|
}
|
||||||
|
if (isKeycloak || isCustomOAuth) {
|
||||||
|
logoutUrl +=
|
||||||
|
"/protocol/openid-connect/logout"
|
||||||
|
+ "?client_id="
|
||||||
|
+ oauth.getClientId()
|
||||||
|
+ "&post_logout_redirect_uri="
|
||||||
|
+ response.encodeRedirectURL(redirectUrl);
|
||||||
|
log.info("Redirecting to Keycloak logout URL: {}", logoutUrl);
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
"No redirect URL for {} available. Redirecting to default logout URL: {}",
|
||||||
|
registrationId,
|
||||||
|
logoutUrl);
|
||||||
|
}
|
||||||
response.sendRedirect(logoutUrl);
|
response.sendRedirect(logoutUrl);
|
||||||
break;
|
}
|
||||||
case "github":
|
case "github", "google" -> {
|
||||||
// Add GitHub specific logout URL if needed
|
log.info(
|
||||||
String githubLogoutUrl = "https://github.com/logout";
|
"No redirect URL for {} available. Redirecting to default logout URL: {}",
|
||||||
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
registrationId,
|
||||||
response.sendRedirect(githubLogoutUrl);
|
redirectUrl);
|
||||||
break;
|
response.sendRedirect(redirectUrl);
|
||||||
case "google":
|
}
|
||||||
// Add Google specific logout URL if needed
|
default -> {
|
||||||
// String googleLogoutUrl =
|
log.info("Redirecting to default logout URL: {}", redirectUrl);
|
||||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
response.sendRedirect(redirectUrl);
|
||||||
// + response.encodeRedirectURL(redirect_url);
|
}
|
||||||
log.info("Google does not have a specific logout URL");
|
|
||||||
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
|
||||||
// response.sendRedirect(googleLogoutUrl);
|
|
||||||
// break;
|
|
||||||
default:
|
|
||||||
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
|
||||||
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
|
||||||
response.sendRedirect(defaultRedirectUrl);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize input to avoid potential security vulnerabilities
|
private static SamlClient getSamlClient(
|
||||||
|
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
|
||||||
|
throws SamlException {
|
||||||
|
String serverUrl =
|
||||||
|
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||||
|
|
||||||
|
String relyingPartyIdentifier =
|
||||||
|
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||||
|
|
||||||
|
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||||
|
|
||||||
|
String idpSLOUrl = samlConf.getIdpSingleLogoutUrl();
|
||||||
|
|
||||||
|
String idpIssuer = samlConf.getIdpIssuer();
|
||||||
|
|
||||||
|
// Create SamlClient instance for SAML logout
|
||||||
|
return new SamlClient(
|
||||||
|
relyingPartyIdentifier,
|
||||||
|
assertionConsumerServiceUrl,
|
||||||
|
idpSLOUrl,
|
||||||
|
idpIssuer,
|
||||||
|
certificates,
|
||||||
|
SamlClient.SamlIdpBinding.POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
||||||
|
* the error request parameter.
|
||||||
|
*
|
||||||
|
* @param request the user's <code>HttpServletRequest</code> request.
|
||||||
|
* @return a <code>String</code> containing the error request parameter.
|
||||||
|
*/
|
||||||
|
private String checkForErrors(HttpServletRequest request) {
|
||||||
|
String errorMessage;
|
||||||
|
String path = "logout=true";
|
||||||
|
|
||||||
|
if (request.getParameter("oAuth2AuthenticationErrorWeb") != null) {
|
||||||
|
path = "errorOAuth=userAlreadyExistsWeb";
|
||||||
|
} else if ((errorMessage = request.getParameter("errorOAuth")) != null) {
|
||||||
|
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
||||||
|
} else if (request.getParameter("oAuth2AutoCreateDisabled") != null) {
|
||||||
|
path = "errorOAuth=oAuth2AutoCreateDisabled";
|
||||||
|
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
|
||||||
|
path = "errorOAuth=oAuth2AdminBlockedUser";
|
||||||
|
} else if (request.getParameter("userIsDisabled") != null) {
|
||||||
|
path = "errorOAuth=userIsDisabled";
|
||||||
|
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||||
|
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
||||||
|
} else if (request.getParameter("badCredentials") != null) {
|
||||||
|
path = "errorOAuth=badCredentials";
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize input to avoid potential security vulnerabilities. Will return a sanitised <code>
|
||||||
|
* String</code>.
|
||||||
|
*
|
||||||
|
* @return a sanitised <code>String</code>
|
||||||
|
*/
|
||||||
private String sanitizeInput(String input) {
|
private String sanitizeInput(String input) {
|
||||||
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
|
|
||||||
import jakarta.servlet.*;
|
import jakarta.servlet.*;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
public class IPRateLimitingFilter implements Filter {
|
public class IPRateLimitingFilter implements Filter {
|
||||||
@@ -24,8 +25,8 @@ public class IPRateLimitingFilter implements Filter {
|
|||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
if (request instanceof HttpServletRequest) {
|
if (request instanceof HttpServletRequest httpServletRequest) {
|
||||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
HttpServletRequest httpRequest = httpServletRequest;
|
||||||
String method = httpRequest.getMethod();
|
String method = httpRequest.getMethod();
|
||||||
String requestURI = httpRequest.getRequestURI();
|
String requestURI = httpRequest.getRequestURI();
|
||||||
// Check if the request is for static resources
|
// Check if the request is for static resources
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ 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.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@@ -34,12 +36,13 @@ public class InitialSecuritySetup {
|
|||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
try {
|
try {
|
||||||
if (databaseService.hasBackup()) {
|
|
||||||
databaseService.importDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userService.hasUsers()) {
|
if (!userService.hasUsers()) {
|
||||||
initializeAdminUser();
|
if (databaseService.hasBackup()) {
|
||||||
|
databaseService.importDatabase();
|
||||||
|
} else {
|
||||||
|
initializeAdminUser();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userService.migrateOauth2ToSSO();
|
userService.migrateOauth2ToSSO();
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.AttemptCounter;
|
import stirling.software.SPDF.model.AttemptCounter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -29,6 +29,7 @@ import org.springframework.security.web.savedrequest.NullRequestCache;
|
|||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import 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;
|
||||||
@@ -50,11 +51,7 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
private final CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
@Qualifier("loginEnabled")
|
|
||||||
private final boolean loginEnabledValue;
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
@Qualifier("runningEE")
|
|
||||||
private final boolean runningEE;
|
private final boolean runningEE;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
@@ -108,6 +105,7 @@ public class SecurityConfiguration {
|
|||||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -163,8 +161,7 @@ public class SecurityConfiguration {
|
|||||||
.logoutSuccessHandler(
|
.logoutSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(applicationProperties))
|
new CustomLogoutSuccessHandler(applicationProperties))
|
||||||
.clearAuthentication(true)
|
.clearAuthentication(true)
|
||||||
.invalidateHttpSession( // Invalidate session
|
.invalidateHttpSession(true)
|
||||||
true)
|
|
||||||
.deleteCookies("JSESSIONID", "remember-me"));
|
.deleteCookies("JSESSIONID", "remember-me"));
|
||||||
http.rememberMe(
|
http.rememberMe(
|
||||||
rememberMeConfigurer -> // Use the configurator directly
|
rememberMeConfigurer -> // Use the configurator directly
|
||||||
@@ -226,14 +223,14 @@ public class SecurityConfiguration {
|
|||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle OAUTH2 Logins
|
// Handle OAUTH2 Logins
|
||||||
if (applicationProperties.getSecurity().isOauth2Activ()) {
|
if (applicationProperties.getSecurity().isOauth2Active()) {
|
||||||
http.oauth2Login(
|
http.oauth2Login(
|
||||||
oauth2 ->
|
oauth2 ->
|
||||||
oauth2.loginPage("/oauth2")
|
oauth2.loginPage("/oauth2")
|
||||||
.
|
.
|
||||||
/*
|
/*
|
||||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||||
is set as true, else login fails with an error message advising the same.
|
is set as true, else login fails with an error message advising the same.
|
||||||
*/
|
*/
|
||||||
successHandler(
|
successHandler(
|
||||||
@@ -257,8 +254,7 @@ public class SecurityConfiguration {
|
|||||||
.permitAll());
|
.permitAll());
|
||||||
}
|
}
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Activ()) {
|
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
|
||||||
// && runningEE
|
|
||||||
// Configure the authentication provider
|
// Configure the authentication provider
|
||||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
new OpenSaml4AuthenticationProvider();
|
new OpenSaml4AuthenticationProvider();
|
||||||
@@ -283,12 +279,13 @@ public class SecurityConfiguration {
|
|||||||
.authenticationRequestResolver(
|
.authenticationRequestResolver(
|
||||||
saml2AuthenticationRequestResolver);
|
saml2AuthenticationRequestResolver);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error configuring SAML2 login", e);
|
log.error("Error configuring SAML 2 login", e);
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log.debug("SAML 2 login is not enabled. Using default.");
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
}
|
}
|
||||||
return http.build();
|
return http.build();
|
||||||
@@ -314,7 +311,7 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public boolean activSecurity() {
|
public boolean activeSecurity() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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;
|
||||||
@@ -86,7 +88,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
// provider for API keys.
|
// provider for API keys.
|
||||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||||
if (!user.isPresent()) {
|
if (user.isEmpty()) {
|
||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter().write("Invalid API Key.");
|
response.getWriter().write("Invalid API Key.");
|
||||||
return;
|
return;
|
||||||
@@ -121,9 +123,11 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||||
response.getWriter()
|
response.getWriter()
|
||||||
.write(
|
.write(
|
||||||
"Authentication required. Please provide a X-API-KEY in request header.\n"
|
"Authentication required. Please provide a X-API-KEY in request"
|
||||||
|
+ " header.\n"
|
||||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
+ "This is found in Settings -> Account Settings -> API Key\n"
|
||||||
+ "Alternatively you can disable authentication if this is unexpected");
|
+ "Alternatively you can disable authentication if this is"
|
||||||
|
+ " unexpected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,21 +143,21 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
// Extract username and determine the login method
|
// 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 detailsUser) {
|
||||||
username = ((UserDetails) principal).getUsername();
|
username = detailsUser.getUsername();
|
||||||
loginMethod = LoginMethod.USERDETAILS;
|
loginMethod = LoginMethod.USERDETAILS;
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
username = ((OAuth2User) principal).getName();
|
username = oAuth2User.getName();
|
||||||
loginMethod = LoginMethod.OAUTH2USER;
|
loginMethod = LoginMethod.OAUTH2USER;
|
||||||
OAUTH2 oAuth = securityProp.getOauth2();
|
OAUTH2 oAuth = securityProp.getOauth2();
|
||||||
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
username = saml2User.name();
|
||||||
loginMethod = LoginMethod.SAML2USER;
|
loginMethod = LoginMethod.SAML2USER;
|
||||||
SAML2 saml2 = securityProp.getSaml2();
|
SAML2 saml2 = securityProp.getSaml2();
|
||||||
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
blockRegistration = saml2 != null && saml2.getBlockRegistration();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String stringUser) {
|
||||||
username = (String) principal;
|
username = stringUser;
|
||||||
loginMethod = LoginMethod.STRINGUSER;
|
loginMethod = LoginMethod.STRINGUSER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +172,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
boolean isUserDisabled = userService.isUserDisabled(username);
|
boolean isUserDisabled = userService.isUserDisabled(username);
|
||||||
|
|
||||||
boolean notSsoLogin =
|
boolean notSsoLogin =
|
||||||
!loginMethod.equals(LoginMethod.OAUTH2USER)
|
!LoginMethod.OAUTH2USER.equals(loginMethod)
|
||||||
&& !loginMethod.equals(LoginMethod.SAML2USER);
|
&& !LoginMethod.SAML2USER.equals(loginMethod);
|
||||||
|
|
||||||
// Block user registration if not allowed by configuration
|
// Block user registration if not allowed by configuration
|
||||||
if (blockRegistration && !isUserExists) {
|
if (blockRegistration && !isUserExists) {
|
||||||
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
|
request.getContextPath() + "/logout?oAuth2AdminBlockedUser=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +195,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// Redirect to logout if credentials are invalid
|
// Redirect to logout if credentials are invalid
|
||||||
if (!isUserExists && notSsoLogin) {
|
if (!isUserExists && notSsoLogin) {
|
||||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
response.sendRedirect(request.getContextPath() + "/logout?badCredentials=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isUserDisabled) {
|
if (isUserDisabled) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ 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.DatabaseInterface;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.*;
|
import stirling.software.SPDF.model.*;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.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;
|
||||||
|
|
||||||
@@ -77,20 +78,18 @@ 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 void processSSOPostLogin(String username, boolean autoCreateUser)
|
||||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
if (existingUser.isPresent()) {
|
if (existingUser.isPresent()) {
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
if (autoCreateUser) {
|
if (autoCreateUser) {
|
||||||
saveUser(username, AuthenticationType.SSO);
|
saveUser(username, AuthenticationType.SSO);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Authentication getAuthentication(String apiKey) {
|
public Authentication getAuthentication(String apiKey) {
|
||||||
@@ -122,12 +121,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public User addApiKeyToUser(String username) {
|
public User addApiKeyToUser(String username) {
|
||||||
Optional<User> user = findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
||||||
if (user.isPresent()) {
|
User user = saveUser(userOpt, generateApiKey());
|
||||||
user.get().setApiKey(generateApiKey());
|
try {
|
||||||
return userRepository.save(user.get());
|
databaseService.exportDatabase();
|
||||||
|
} catch (SQLException | UnsupportedProviderException e) {
|
||||||
|
log.error("Error exporting database after adding API key to user", e);
|
||||||
}
|
}
|
||||||
throw new UsernameNotFoundException("User not found");
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User refreshApiKeyForUser(String username) {
|
public User refreshApiKeyForUser(String username) {
|
||||||
@@ -139,6 +140,9 @@ public class UserService implements UserServiceInterface {
|
|||||||
User user =
|
User user =
|
||||||
findByUsernameIgnoreCase(username)
|
findByUsernameIgnoreCase(username)
|
||||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||||
|
if (user.getApiKey() == null || user.getApiKey().length() == 0) {
|
||||||
|
user = addApiKeyToUser(username);
|
||||||
|
}
|
||||||
return user.getApiKey();
|
return user.getApiKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,6 +173,14 @@ public class UserService implements UserServiceInterface {
|
|||||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private User saveUser(Optional<User> user, String apiKey) {
|
||||||
|
if (user.isPresent()) {
|
||||||
|
user.get().setApiKey(apiKey);
|
||||||
|
return userRepository.save(user.get());
|
||||||
|
}
|
||||||
|
throw new UsernameNotFoundException("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
@@ -369,21 +381,18 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public void invalidateUserSessions(String username) {
|
public void invalidateUserSessions(String username) {
|
||||||
String usernameP = "";
|
String usernameP = "";
|
||||||
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
for (SessionInformation sessionsInformation :
|
for (SessionInformation sessionsInformation :
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
sessionRegistry.getAllSessions(principal, false)) {
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
usernameP = detailsUser.getUsername();
|
||||||
usernameP = userDetails.getUsername();
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
} else if (principal instanceof OAuth2User) {
|
|
||||||
OAuth2User oAuth2User = (OAuth2User) principal;
|
|
||||||
usernameP = oAuth2User.getName();
|
usernameP = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
CustomSaml2AuthenticatedPrincipal saml2User =
|
usernameP = saml2User.name();
|
||||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
} else if (principal instanceof String stringUser) {
|
||||||
usernameP = saml2User.getName();
|
usernameP = stringUser;
|
||||||
} else if (principal instanceof String) {
|
|
||||||
usernameP = (String) principal;
|
|
||||||
}
|
}
|
||||||
if (usernameP.equalsIgnoreCase(username)) {
|
if (usernameP.equalsIgnoreCase(username)) {
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||||
@@ -394,49 +403,56 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
public String getCurrentUsername() {
|
public String getCurrentUsername() {
|
||||||
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
return ((UserDetails) principal).getUsername();
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
} else if (principal instanceof OAuth2User) {
|
return detailsUser.getUsername();
|
||||||
return ((OAuth2User) principal)
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
.getAttribute(
|
return oAuth2User.getAttribute(
|
||||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
return saml2User.name();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String stringUser) {
|
||||||
return (String) principal;
|
return stringUser;
|
||||||
} else {
|
|
||||||
return principal.toString();
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void syncCustomApiUser(String customApiKey)
|
public void syncCustomApiUser(String customApiKey) {
|
||||||
throws SQLException, UnsupportedProviderException {
|
if (customApiKey == null || customApiKey.trim().isBlank()) {
|
||||||
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String username = "CUSTOM_API_USER";
|
String username = "CUSTOM_API_USER";
|
||||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||||
if (!existingUser.isPresent()) {
|
|
||||||
// Create new user with API role
|
existingUser.ifPresentOrElse(
|
||||||
User user = new User();
|
user -> {
|
||||||
user.setUsername(username);
|
// Update API key if it has changed
|
||||||
user.setPassword(UUID.randomUUID().toString());
|
User updatedUser = existingUser.get();
|
||||||
user.setEnabled(true);
|
|
||||||
user.setFirstLogin(false);
|
if (!customApiKey.equals(updatedUser.getApiKey())) {
|
||||||
user.setAuthenticationType(AuthenticationType.WEB);
|
updatedUser.setApiKey(customApiKey);
|
||||||
user.setApiKey(customApiKey);
|
userRepository.save(updatedUser);
|
||||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
}
|
||||||
userRepository.save(user);
|
},
|
||||||
|
() -> {
|
||||||
|
// Create new user with API role
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(UUID.randomUUID().toString());
|
||||||
|
user.setEnabled(true);
|
||||||
|
user.setFirstLogin(false);
|
||||||
|
user.setAuthenticationType(AuthenticationType.WEB);
|
||||||
|
user.setApiKey(customApiKey);
|
||||||
|
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||||
|
userRepository.save(user);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
databaseService.exportDatabase();
|
databaseService.exportDatabase();
|
||||||
} else {
|
} catch (SQLException | UnsupportedProviderException e) {
|
||||||
// Update API key if it has changed
|
log.error("Error exporting database after synchronising custom API user", e);
|
||||||
User user = existingUser.get();
|
|
||||||
if (!customApiKey.equals(user.getApiKey())) {
|
|
||||||
user.setApiKey(customApiKey);
|
|
||||||
userRepository.save(user);
|
|
||||||
databaseService.exportDatabase();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package stirling.software.SPDF.config.security.database;
|
package stirling.software.SPDF.config.security.database;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
@@ -11,9 +9,10 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
@@ -36,8 +35,8 @@ public class DatabaseConfig {
|
|||||||
DATASOURCE_DEFAULT_URL =
|
DATASOURCE_DEFAULT_URL =
|
||||||
"jdbc:h2:file:"
|
"jdbc:h2:file:"
|
||||||
+ InstallationPathConfig.getConfigPath()
|
+ InstallationPathConfig.getConfigPath()
|
||||||
+ File.separator
|
|
||||||
+ "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
+ "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
||||||
|
log.debug("Database URL: {}", DATASOURCE_DEFAULT_URL);
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.runningEE = runningEE;
|
this.runningEE = runningEE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.jdbc.datasource.init.ScriptException;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||||
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Conditional(H2SQLCondition.class)
|
@Conditional(H2SQLCondition.class)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.security.web.authentication.SimpleUrlAuthenticationFa
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -28,7 +29,7 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
|
|
||||||
if (exception instanceof BadCredentialsException) {
|
if (exception instanceof BadCredentialsException) {
|
||||||
log.error("BadCredentialsException", exception);
|
log.error("BadCredentialsException", exception);
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof DisabledException) {
|
if (exception instanceof DisabledException) {
|
||||||
@@ -41,18 +42,20 @@ public class CustomOAuth2AuthenticationFailureHandler
|
|||||||
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (exception instanceof OAuth2AuthenticationException) {
|
if (exception instanceof OAuth2AuthenticationException oAuth2Exception) {
|
||||||
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
|
OAuth2Error error = oAuth2Exception.getError();
|
||||||
|
|
||||||
String errorCode = error.getErrorCode();
|
String errorCode = error.getErrorCode();
|
||||||
|
|
||||||
if (error.getErrorCode().equals("Password must not be null")) {
|
if ("Password must not be null".equals(error.getErrorCode())) {
|
||||||
errorCode = "userAlreadyExistsWeb";
|
errorCode = "userAlreadyExistsWeb";
|
||||||
}
|
}
|
||||||
log.error("OAuth2 Authentication error: " + errorCode);
|
|
||||||
log.error("OAuth2AuthenticationException", exception);
|
log.error(
|
||||||
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
|
"OAuth2 Authentication error: {}",
|
||||||
return;
|
errorCode != null ? errorCode : exception.getMessage(),
|
||||||
|
exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
|
||||||
}
|
}
|
||||||
log.error("Unhandled authentication exception", exception);
|
log.error("Unhandled authentication exception", exception);
|
||||||
super.onAuthenticationFailure(request, response, exception);
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
|||||||
@@ -14,24 +14,24 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
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.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
public class CustomOAuth2AuthenticationSuccessHandler
|
public class CustomOAuth2AuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
private ApplicationProperties applicationProperties;
|
private final UserService userService;
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
public CustomOAuth2AuthenticationSuccessHandler(
|
public CustomOAuth2AuthenticationSuccessHandler(
|
||||||
final LoginAttemptService loginAttemptService,
|
LoginAttemptService loginAttemptService,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
UserService userService) {
|
UserService userService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
@@ -47,12 +47,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = "";
|
String username = "";
|
||||||
|
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User oAuth2User) {
|
||||||
OAuth2User oauthUser = (OAuth2User) principal;
|
username = oAuth2User.getName();
|
||||||
username = oauthUser.getName();
|
} else if (principal instanceof UserDetails detailsUser) {
|
||||||
} else if (principal instanceof UserDetails) {
|
username = detailsUser.getUsername();
|
||||||
UserDetails oauthUser = (UserDetails) principal;
|
|
||||||
username = oauthUser.getUsername();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
@@ -77,6 +75,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userService.isUserDisabled(username)) {
|
if (userService.isUserDisabled(username)) {
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
@@ -86,13 +85,14 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (oAuth.getBlockRegistration()
|
if (oAuth.getBlockRegistration()
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User) {
|
||||||
|
|||||||
@@ -12,23 +12,24 @@ import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
|
|||||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
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.ApplicationProperties.Security.OAUTH2.Client;
|
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
|
import stirling.software.SPDF.model.UsernameAttribute;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||||
|
|
||||||
private final OidcUserService delegate = new OidcUserService();
|
private final OidcUserService delegate = new OidcUserService();
|
||||||
|
|
||||||
private UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private final LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public CustomOAuth2UserService(
|
public CustomOAuth2UserService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@@ -41,34 +42,26 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
|
||||||
String usernameAttribute = oauth2.getUseAsUsername();
|
|
||||||
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
|
||||||
Client client = oauth2.getClient();
|
|
||||||
if (client != null && client.getKeycloak() != null) {
|
|
||||||
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
|
||||||
} else {
|
|
||||||
usernameAttribute = "email";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OidcUser user = delegate.loadUser(userRequest);
|
OidcUser user = delegate.loadUser(userRequest);
|
||||||
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||||
|
UsernameAttribute usernameAttribute =
|
||||||
|
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
||||||
|
String usernameAttributeKey = usernameAttribute.getName();
|
||||||
|
|
||||||
// Check if the username claim is null or empty
|
// todo: save user by OIDC ID instead of username
|
||||||
if (username == null || username.trim().isEmpty()) {
|
Optional<User> internalUser =
|
||||||
throw new IllegalArgumentException(
|
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
|
||||||
"Claim '" + usernameAttribute + "' cannot be null or empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
if (internalUser.isPresent()) {
|
||||||
if (duser.isPresent()) {
|
String internalUsername = internalUser.get().getUsername();
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(internalUsername)) {
|
||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"The account "
|
||||||
|
+ internalUsername
|
||||||
|
+ " has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
if (userService.hasPassword(username)) {
|
if (userService.hasPassword(usernameAttributeKey)) {
|
||||||
throw new IllegalArgumentException("Password must not be null");
|
throw new IllegalArgumentException("Password must not be null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +71,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
|||||||
user.getAuthorities(),
|
user.getAuthorities(),
|
||||||
userRequest.getIdToken(),
|
userRequest.getIdToken(),
|
||||||
user.getUserInfo(),
|
user.getUserInfo(),
|
||||||
usernameAttribute);
|
usernameAttributeKey);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.error("Error loading OIDC user: {}", e.getMessage());
|
log.error("Error loading OIDC user: {}", e.getMessage());
|
||||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package stirling.software.SPDF.config.security.oauth2;
|
package stirling.software.SPDF.config.security.oauth2;
|
||||||
|
|
||||||
|
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
|
||||||
|
import static stirling.software.SPDF.utils.validation.Validator.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -20,23 +23,26 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
||||||
|
|
||||||
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.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
import stirling.software.SPDF.model.UsernameAttribute;
|
||||||
|
import stirling.software.SPDF.model.exception.NoProviderFoundException;
|
||||||
|
import stirling.software.SPDF.model.provider.GitHubProvider;
|
||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
|
import stirling.software.SPDF.model.provider.Provider;
|
||||||
|
|
||||||
@Configuration
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(
|
@Configuration
|
||||||
value = "security.oauth2.enabled",
|
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public class OAuth2Configuration {
|
public class OAuth2Configuration {
|
||||||
|
|
||||||
|
public static final String REDIRECT_URI_PATH = "{baseUrl}/login/oauth2/code/";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
@Lazy private final UserService userService;
|
@Lazy private final UserService userService;
|
||||||
|
|
||||||
@@ -47,139 +53,175 @@ public class OAuth2Configuration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||||
value = "security.oauth2.enabled",
|
public ClientRegistrationRepository clientRegistrationRepository()
|
||||||
havingValue = "true",
|
throws NoProviderFoundException {
|
||||||
matchIfMissing = false)
|
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
|
||||||
List<ClientRegistration> registrations = new ArrayList<>();
|
List<ClientRegistration> registrations = new ArrayList<>();
|
||||||
githubClientRegistration().ifPresent(registrations::add);
|
githubClientRegistration().ifPresent(registrations::add);
|
||||||
oidcClientRegistration().ifPresent(registrations::add);
|
oidcClientRegistration().ifPresent(registrations::add);
|
||||||
googleClientRegistration().ifPresent(registrations::add);
|
googleClientRegistration().ifPresent(registrations::add);
|
||||||
keycloakClientRegistration().ifPresent(registrations::add);
|
keycloakClientRegistration().ifPresent(registrations::add);
|
||||||
|
|
||||||
if (registrations.isEmpty()) {
|
if (registrations.isEmpty()) {
|
||||||
log.error("At least one OAuth2 provider must be configured");
|
log.error("No OAuth2 provider registered");
|
||||||
System.exit(1);
|
throw new NoProviderFoundException("At least one OAuth2 provider must be configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new InMemoryClientRegistrationRepository(registrations);
|
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() {
|
private Optional<ClientRegistration> keycloakClientRegistration() {
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
|
if (isOAuth2Enabled(oauth2) || isClientInitialised(oauth2)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
Client client = oauth2.getClient();
|
||||||
return Optional.empty();
|
KeycloakProvider keycloakClient = client.getKeycloak();
|
||||||
}
|
Provider keycloak =
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
new KeycloakProvider(
|
||||||
return keycloak != null && keycloak.isSettingsValid()
|
keycloakClient.getIssuer(),
|
||||||
|
keycloakClient.getClientId(),
|
||||||
|
keycloakClient.getClientSecret(),
|
||||||
|
keycloakClient.getScopes(),
|
||||||
|
keycloakClient.getUseAsUsername());
|
||||||
|
|
||||||
|
return validateProvider(keycloak)
|
||||||
? Optional.of(
|
? Optional.of(
|
||||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||||
.registrationId(keycloak.getName())
|
.registrationId(keycloak.getName())
|
||||||
.clientId(keycloak.getClientId())
|
.clientId(keycloak.getClientId())
|
||||||
.clientSecret(keycloak.getClientSecret())
|
.clientSecret(keycloak.getClientSecret())
|
||||||
.scope(keycloak.getScopes())
|
.scope(keycloak.getScopes())
|
||||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
.userNameAttributeName(keycloak.getUseAsUsername().getName())
|
||||||
.clientName(keycloak.getClientName())
|
.clientName(keycloak.getClientName())
|
||||||
.build())
|
.build())
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<ClientRegistration> googleClientRegistration() {
|
||||||
|
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
||||||
|
|
||||||
|
if (isOAuth2Enabled(oAuth2) || isClientInitialised(oAuth2)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Client client = oAuth2.getClient();
|
||||||
|
GoogleProvider googleClient = client.getGoogle();
|
||||||
|
Provider google =
|
||||||
|
new GoogleProvider(
|
||||||
|
googleClient.getClientId(),
|
||||||
|
googleClient.getClientSecret(),
|
||||||
|
googleClient.getScopes(),
|
||||||
|
googleClient.getUseAsUsername());
|
||||||
|
|
||||||
|
return validateProvider(google)
|
||||||
|
? 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().getName())
|
||||||
|
.clientName(google.getClientName())
|
||||||
|
.redirectUri(REDIRECT_URI_PATH + google.getName())
|
||||||
|
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
private Optional<ClientRegistration> githubClientRegistration() {
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
|
if (isOAuth2Enabled(oAuth2)) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
Client client = oAuth2.getClient();
|
||||||
return Optional.empty();
|
GitHubProvider githubClient = client.getGithub();
|
||||||
}
|
Provider github =
|
||||||
GithubProvider github = client.getGithub();
|
new GitHubProvider(
|
||||||
return github != null && github.isSettingsValid()
|
githubClient.getClientId(),
|
||||||
|
githubClient.getClientSecret(),
|
||||||
|
githubClient.getScopes(),
|
||||||
|
githubClient.getUseAsUsername());
|
||||||
|
|
||||||
|
return validateProvider(github)
|
||||||
? Optional.of(
|
? Optional.of(
|
||||||
ClientRegistration.withRegistrationId(github.getName())
|
ClientRegistration.withRegistrationId(github.getName())
|
||||||
.clientId(github.getClientId())
|
.clientId(github.getClientId())
|
||||||
.clientSecret(github.getClientSecret())
|
.clientSecret(github.getClientSecret())
|
||||||
.scope(github.getScopes())
|
.scope(github.getScopes())
|
||||||
.authorizationUri(github.getAuthorizationuri())
|
.authorizationUri(github.getAuthorizationUri())
|
||||||
.tokenUri(github.getTokenuri())
|
.tokenUri(github.getTokenUri())
|
||||||
.userInfoUri(github.getUserinfouri())
|
.userInfoUri(github.getUserInfoUri())
|
||||||
.userNameAttributeName(github.getUseAsUsername())
|
.userNameAttributeName(github.getUseAsUsername().getName())
|
||||||
.clientName(github.getClientName())
|
.clientName(github.getClientName())
|
||||||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
.redirectUri(REDIRECT_URI_PATH + github.getName())
|
||||||
.authorizationGrantType(
|
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||||
org.springframework.security.oauth2.core
|
|
||||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
|
||||||
.build())
|
.build())
|
||||||
: Optional.empty();
|
: Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<ClientRegistration> oidcClientRegistration() {
|
private Optional<ClientRegistration> oidcClientRegistration() {
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||||
if (oauth == null
|
|
||||||
|| oauth.getIssuer() == null
|
if (isOAuth2Enabled(oauth) || isClientInitialised(oauth)) {
|
||||||
|| 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.empty();
|
||||||
}
|
}
|
||||||
return Optional.of(
|
|
||||||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
String name = oauth.getProvider();
|
||||||
.registrationId("oidc")
|
String firstChar = String.valueOf(name.charAt(0));
|
||||||
.clientId(oauth.getClientId())
|
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
|
||||||
.clientSecret(oauth.getClientSecret())
|
|
||||||
.scope(oauth.getScopes())
|
Provider oidcProvider =
|
||||||
.userNameAttributeName(oauth.getUseAsUsername())
|
new Provider(
|
||||||
.clientName("OIDC")
|
oauth.getIssuer(),
|
||||||
.build());
|
name,
|
||||||
|
clientName,
|
||||||
|
oauth.getClientId(),
|
||||||
|
oauth.getClientSecret(),
|
||||||
|
oauth.getScopes(),
|
||||||
|
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
|
||||||
|
oauth.getLogoutUrl(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null);
|
||||||
|
|
||||||
|
return !isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider)
|
||||||
|
? Optional.of(
|
||||||
|
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||||
|
.registrationId(name)
|
||||||
|
.clientId(oidcProvider.getClientId())
|
||||||
|
.clientSecret(oidcProvider.getClientSecret())
|
||||||
|
.scope(oidcProvider.getScopes())
|
||||||
|
.userNameAttributeName(oidcProvider.getUseAsUsername().getName())
|
||||||
|
.clientName(clientName)
|
||||||
|
.redirectUri(REDIRECT_URI_PATH + "oidc")
|
||||||
|
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||||
|
.build())
|
||||||
|
: Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOAuth2Enabled(OAUTH2 oAuth2) {
|
||||||
|
return oAuth2 == null || !oAuth2.getEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isClientInitialised(OAUTH2 oauth2) {
|
||||||
|
Client client = oauth2.getClient();
|
||||||
|
return client == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||||
value = "security.oauth2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||||
return (authorities) -> {
|
return (authorities) -> {
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||||
@@ -188,7 +230,7 @@ public class OAuth2Configuration {
|
|||||||
// Add existing OAUTH2 Authorities
|
// Add existing OAUTH2 Authorities
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
||||||
// Add Authorities from database for existing user, if user is present.
|
// Add Authorities from database for existing user, if user is present.
|
||||||
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
if (authority instanceof OAuth2UserAuthority oAuth2Auth) {
|
||||||
String useAsUsername =
|
String useAsUsername =
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getSecurity()
|
.getSecurity()
|
||||||
@@ -196,14 +238,12 @@ public class OAuth2Configuration {
|
|||||||
.getUseAsUsername();
|
.getUseAsUsername();
|
||||||
Optional<User> userOpt =
|
Optional<User> userOpt =
|
||||||
userService.findByUsernameIgnoreCase(
|
userService.findByUsernameIgnoreCase(
|
||||||
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user != null) {
|
mappedAuthorities.add(
|
||||||
mappedAuthorities.add(
|
new SimpleGrantedAuthority(
|
||||||
new SimpleGrantedAuthority(
|
userService.findRole(user).getAuthority()));
|
||||||
userService.findRole(user).getAuthority()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import org.bouncycastle.openssl.PEMParser;
|
|||||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||||
import org.bouncycastle.util.io.pem.PemObject;
|
import org.bouncycastle.util.io.pem.PemObject;
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
import org.bouncycastle.util.io.pem.PemReader;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public class CertificateUtils {
|
public class CertificateUtils {
|
||||||
|
|
||||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||||
@@ -38,13 +40,12 @@ public class CertificateUtils {
|
|||||||
Object object = pemParser.readObject();
|
Object object = pemParser.readObject();
|
||||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||||
|
|
||||||
if (object instanceof PEMKeyPair) {
|
if (object instanceof PEMKeyPair keypair) {
|
||||||
// Handle traditional RSA private key format
|
// Handle traditional RSA private key format
|
||||||
PEMKeyPair keypair = (PEMKeyPair) object;
|
|
||||||
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
||||||
} else if (object instanceof PrivateKeyInfo) {
|
} else if (object instanceof PrivateKeyInfo keyInfo) {
|
||||||
// Handle PKCS#8 format
|
// Handle PKCS#8 format
|
||||||
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
|
return (RSAPrivateKey) converter.getPrivateKey(keyInfo);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Unsupported key format: "
|
"Unsupported key format: "
|
||||||
|
|||||||
@@ -4,27 +4,17 @@ import java.io.Serializable;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||||
|
|
||||||
public class CustomSaml2AuthenticatedPrincipal
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
|
public record CustomSaml2AuthenticatedPrincipal(
|
||||||
|
String name,
|
||||||
|
Map<String, List<Object>> attributes,
|
||||||
|
String nameId,
|
||||||
|
List<String> sessionIndexes)
|
||||||
implements Saml2AuthenticatedPrincipal, Serializable {
|
implements Saml2AuthenticatedPrincipal, Serializable {
|
||||||
|
|
||||||
private final String name;
|
|
||||||
private final Map<String, List<Object>> attributes;
|
|
||||||
private final String nameId;
|
|
||||||
private final List<String> sessionIndexes;
|
|
||||||
|
|
||||||
public CustomSaml2AuthenticatedPrincipal(
|
|
||||||
String name,
|
|
||||||
Map<String, List<Object>> attributes,
|
|
||||||
String nameId,
|
|
||||||
List<String> sessionIndexes) {
|
|
||||||
this.name = name;
|
|
||||||
this.attributes = attributes;
|
|
||||||
this.nameId = nameId;
|
|
||||||
this.sessionIndexes = sessionIndexes;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return this.name;
|
return this.name;
|
||||||
@@ -34,12 +24,4 @@ public class CustomSaml2AuthenticatedPrincipal
|
|||||||
public Map<String, List<Object>> getAttributes() {
|
public Map<String, List<Object>> getAttributes() {
|
||||||
return this.attributes;
|
return this.attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNameId() {
|
|
||||||
return this.nameId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<String> getSessionIndexes() {
|
|
||||||
return this.sessionIndexes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,20 @@ package stirling.software.SPDF.config.security.saml2;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.saml2.core.Saml2Error;
|
import org.springframework.security.saml2.core.Saml2Error;
|
||||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||||
|
|
||||||
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -21,18 +23,19 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
|||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException {
|
||||||
|
log.error("Authentication error", exception);
|
||||||
|
|
||||||
if (exception instanceof Saml2AuthenticationException) {
|
if (exception instanceof Saml2AuthenticationException) {
|
||||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
.sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode());
|
||||||
} else if (exception instanceof ProviderNotFoundException) {
|
} else if (exception instanceof ProviderNotFoundException) {
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(
|
.sendRedirect(
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
"/login?erroroauth=not_authentication_provider_found");
|
"/login?errorOAuth=not_authentication_provider_found");
|
||||||
}
|
}
|
||||||
log.error("AuthenticationException: " + exception);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,16 @@ import jakarta.servlet.ServletException;
|
|||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
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.model.exception.UnsupportedProviderException;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -39,8 +41,8 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
log.debug("Starting SAML2 authentication success handling");
|
log.debug("Starting SAML2 authentication success handling");
|
||||||
|
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2Principal) {
|
||||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
String username = saml2Principal.name();
|
||||||
log.debug("Authenticated principal found for user: {}", username);
|
log.debug("Authenticated principal found for user: {}", username);
|
||||||
|
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
@@ -95,7 +97,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,20 +105,18 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
if (saml2.getBlockRegistration() && !userExists) {
|
if (saml2.getBlockRegistration() && !userExists) {
|
||||||
log.debug("Registration blocked for new user: {}", username);
|
log.debug("Registration blocked for new user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.debug("Processing SSO post-login for user: {}", username);
|
log.debug("Processing SSO post-login for user: {}", username);
|
||||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||||
log.debug("Successfully processed authentication for user: {}", username);
|
log.debug("Successfully processed authentication for user: {}", username);
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
|
||||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||||
log.debug(
|
log.debug(
|
||||||
"Invalid username detected for user: {}, redirecting to logout",
|
"Invalid username detected for user: {}, redirecting to logout",
|
||||||
username);
|
username);
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,20 +7,23 @@ import org.opensaml.saml.saml2.core.Assertion;
|
|||||||
import org.opensaml.saml.saml2.core.Attribute;
|
import org.opensaml.saml.saml2.core.Attribute;
|
||||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||||
import org.opensaml.saml.saml2.core.AuthnStatement;
|
import org.opensaml.saml.saml2.core.AuthnStatement;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.core.convert.converter.Converter;
|
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 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;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
public class CustomSaml2ResponseAuthenticationConverter
|
public class CustomSaml2ResponseAuthenticationConverter
|
||||||
implements Converter<ResponseToken, Saml2Authentication> {
|
implements Converter<ResponseToken, Saml2Authentication> {
|
||||||
|
|
||||||
private UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
@@ -60,10 +63,10 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||||
|
|
||||||
// Debug log with actual values
|
// Debug log with actual values
|
||||||
log.debug("Extracted SAML Attributes: " + attributes);
|
log.debug("Extracted SAML Attributes: {}", attributes);
|
||||||
|
|
||||||
// Try to get username/identifier in order of preference
|
// Try to get username/identifier in order of preference
|
||||||
String userIdentifier = null;
|
String userIdentifier;
|
||||||
if (hasAttribute(attributes, "username")) {
|
if (hasAttribute(attributes, "username")) {
|
||||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||||
@@ -83,10 +86,8 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user != null) {
|
simpleGrantedAuthority =
|
||||||
simpleGrantedAuthority =
|
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> sessionIndexes = new ArrayList<>();
|
List<String> sessionIndexes = new ArrayList<>();
|
||||||
@@ -101,7 +102,7 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
return new Saml2Authentication(
|
return new Saml2Authentication(
|
||||||
principal,
|
principal,
|
||||||
responseToken.getToken().getSaml2Response(),
|
responseToken.getToken().getSaml2Response(),
|
||||||
Collections.singletonList(simpleGrantedAuthority));
|
List.of(simpleGrantedAuthority));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||||
|
|||||||
@@ -11,40 +11,37 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
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.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.registration.Saml2MessageBinding;
|
||||||
|
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
|
||||||
value = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public class SAML2Configuration {
|
public class SAML2Configuration {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
||||||
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
|
||||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||||
Resource certificateResource = samlConf.getSpCert();
|
Resource certificateResource = samlConf.getSpCert();
|
||||||
@@ -56,81 +53,124 @@ public class SAML2Configuration {
|
|||||||
RelyingPartyRegistration rp =
|
RelyingPartyRegistration rp =
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||||
.signingX509Credentials(c -> c.add(signingCredential))
|
.signingX509Credentials(c -> c.add(signingCredential))
|
||||||
|
.entityId(samlConf.getIdpIssuer())
|
||||||
|
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
||||||
|
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
||||||
|
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
|
||||||
|
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
||||||
|
.assertionConsumerServiceLocation(
|
||||||
|
"{baseUrl}/login/saml2/sso/{registrationId}")
|
||||||
.assertingPartyMetadata(
|
.assertingPartyMetadata(
|
||||||
metadata ->
|
metadata ->
|
||||||
metadata.entityId(samlConf.getIdpIssuer())
|
metadata.entityId(samlConf.getIdpIssuer())
|
||||||
.singleSignOnServiceLocation(
|
|
||||||
samlConf.getIdpSingleLoginUrl())
|
|
||||||
.verificationX509Credentials(
|
.verificationX509Credentials(
|
||||||
c -> c.add(verificationCredential))
|
c -> c.add(verificationCredential))
|
||||||
.singleSignOnServiceBinding(
|
.singleSignOnServiceBinding(
|
||||||
Saml2MessageBinding.POST)
|
Saml2MessageBinding.POST)
|
||||||
|
.singleSignOnServiceLocation(
|
||||||
|
samlConf.getIdpSingleLoginUrl())
|
||||||
|
.singleLogoutServiceBinding(
|
||||||
|
Saml2MessageBinding.POST)
|
||||||
|
.singleLogoutServiceLocation(
|
||||||
|
samlConf.getIdpSingleLogoutUrl())
|
||||||
.wantAuthnRequestsSigned(true))
|
.wantAuthnRequestsSigned(true))
|
||||||
.build();
|
.build();
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||||
name = "security.saml2.enabled",
|
|
||||||
havingValue = "true",
|
|
||||||
matchIfMissing = false)
|
|
||||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||||
OpenSaml4AuthenticationRequestResolver resolver =
|
OpenSaml4AuthenticationRequestResolver resolver =
|
||||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||||
|
|
||||||
resolver.setAuthnRequestCustomizer(
|
resolver.setAuthnRequestCustomizer(
|
||||||
customizer -> {
|
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();
|
HttpServletRequest request = customizer.getRequest();
|
||||||
// Log HTTP request details
|
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
|
||||||
log.debug("Request URI: {}", request.getRequestURI());
|
new HttpSessionSaml2AuthenticationRequestRepository();
|
||||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
||||||
log.debug("Query String: {}", request.getQueryString());
|
requestRepository.loadAuthenticationRequest(request);
|
||||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
|
||||||
// Log headers
|
if (saml2AuthenticationRequest != null) {
|
||||||
Collections.list(request.getHeaderNames())
|
String sessionId = request.getSession(false).getId();
|
||||||
.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(
|
log.debug(
|
||||||
"AssertionConsumerServiceURL: {}",
|
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
|
||||||
authnRequest.getAssertionConsumerServiceURL());
|
sessionId);
|
||||||
}
|
|
||||||
// Log NameID policy if present
|
String authenticationRequestId = saml2AuthenticationRequest.getId();
|
||||||
if (authnRequest.getNameIDPolicy() != null) {
|
|
||||||
log.debug(
|
if (!authenticationRequestId.isBlank()) {
|
||||||
"NameIDPolicy Format: {}",
|
authnRequest.setID(authenticationRequestId);
|
||||||
authnRequest.getNameIDPolicy().getFormat());
|
} else {
|
||||||
|
log.warn(
|
||||||
|
"No authentication request found for HTTP session {}. Generating new ID",
|
||||||
|
sessionId);
|
||||||
|
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.debug("Generating new authentication request ID");
|
||||||
|
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logAuthnRequestDetails(authnRequest);
|
||||||
|
logHttpRequestDetails(request);
|
||||||
});
|
});
|
||||||
return resolver;
|
return resolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
|
||||||
|
String message =
|
||||||
|
"""
|
||||||
|
AuthnRequest:
|
||||||
|
|
||||||
|
ID: {}
|
||||||
|
Issuer: {}
|
||||||
|
IssueInstant: {}
|
||||||
|
AssertionConsumerService (ACS) URL: {}
|
||||||
|
""";
|
||||||
|
log.debug(
|
||||||
|
message,
|
||||||
|
authnRequest.getID(),
|
||||||
|
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
|
||||||
|
authnRequest.getIssueInstant(),
|
||||||
|
authnRequest.getAssertionConsumerServiceURL());
|
||||||
|
|
||||||
|
if (authnRequest.getNameIDPolicy() != null) {
|
||||||
|
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void logHttpRequestDetails(HttpServletRequest request) {
|
||||||
|
log.debug("HTTP Headers: ");
|
||||||
|
Collections.list(request.getHeaderNames())
|
||||||
|
.forEach(
|
||||||
|
headerName ->
|
||||||
|
log.debug("{}: {}", headerName, request.getHeader(headerName)));
|
||||||
|
String message =
|
||||||
|
"""
|
||||||
|
HTTP Request Method: {}
|
||||||
|
Session ID: {}
|
||||||
|
Request Path: {}
|
||||||
|
Query String: {}
|
||||||
|
Remote Address: {}
|
||||||
|
|
||||||
|
SAML Request Parameters:
|
||||||
|
|
||||||
|
SAMLRequest: {}
|
||||||
|
RelayState: {}
|
||||||
|
""";
|
||||||
|
log.debug(
|
||||||
|
message,
|
||||||
|
request.getMethod(),
|
||||||
|
request.getSession().getId(),
|
||||||
|
request.getRequestURI(),
|
||||||
|
request.getQueryString(),
|
||||||
|
request.getRemoteAddr(),
|
||||||
|
request.getParameter("SAMLRequest"),
|
||||||
|
request.getParameter("RelayState"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.springframework.stereotype.Component;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@@ -42,14 +43,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||||
String principalName = null;
|
String principalName = null;
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
principalName = ((UserDetails) principal).getUsername();
|
principalName = detailsUser.getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
principalName = ((OAuth2User) principal).getName();
|
principalName = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
principalName = saml2User.name();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String stringUser) {
|
||||||
principalName = (String) principal;
|
principalName = stringUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
@@ -73,14 +74,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
public void registerNewSession(String sessionId, Object principal) {
|
public void registerNewSession(String sessionId, Object principal) {
|
||||||
String principalName = null;
|
String principalName = null;
|
||||||
|
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
principalName = ((UserDetails) principal).getUsername();
|
principalName = detailsUser.getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
principalName = ((OAuth2User) principal).getName();
|
principalName = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
principalName = saml2User.name();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String stringUser) {
|
||||||
principalName = (String) principal;
|
principalName = stringUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.springframework.data.repository.query.Param;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SessionEntity;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import stirling.software.SPDF.service.LanguageService;
|
import stirling.software.SPDF.service.LanguageService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ 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.DatabaseService;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.SortTypes;
|
import stirling.software.SPDF.model.SortTypes;
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||||
|
|||||||
@@ -31,14 +31,14 @@ public class SettingsController {
|
|||||||
@PostMapping("/update-enable-analytics")
|
@PostMapping("/update-enable-analytics")
|
||||||
@Hidden
|
@Hidden
|
||||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||||
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||||
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 "
|
||||||
+ InstallationPathConfig.getSettingsPath());
|
+ InstallationPathConfig.getSettingsPath());
|
||||||
}
|
}
|
||||||
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
|
||||||
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
applicationProperties.getSystem().setEnableAnalytics(enabled);
|
||||||
return ResponseEntity.ok("Updated");
|
return ResponseEntity.ok("Updated");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PdfMetadata;
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||||
import stirling.software.SPDF.service.PdfMetadataService;
|
import stirling.software.SPDF.service.PdfMetadataService;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
@@ -34,7 +36,7 @@ import stirling.software.SPDF.model.AuthenticationType;
|
|||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "User", description = "User APIs")
|
@Tag(name = "User", description = "User APIs")
|
||||||
@@ -124,7 +126,7 @@ public class UserController {
|
|||||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -152,7 +154,7 @@ public class UserController {
|
|||||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||||
}
|
}
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||||
if (userOpt == null || userOpt.isEmpty()) {
|
if (userOpt.isEmpty()) {
|
||||||
return new RedirectView("/account?messageType=userNotFound", true);
|
return new RedirectView("/account?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -174,7 +176,7 @@ public class UserController {
|
|||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
}
|
}
|
||||||
log.debug("Processed updates: " + updates);
|
log.debug("Processed updates: {}", updates);
|
||||||
// Assuming you have a method in userService to update the settings for a user
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
userService.updateUserSettings(principal.getName(), updates);
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
// Redirect to a page of your choice after updating
|
// Redirect to a page of your choice after updating
|
||||||
@@ -197,7 +199,7 @@ public class UserController {
|
|||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
if (user.getUsername().equalsIgnoreCase(username)) {
|
||||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -274,7 +276,7 @@ public class UserController {
|
|||||||
Authentication authentication)
|
Authentication authentication)
|
||||||
throws SQLException, UnsupportedProviderException {
|
throws SQLException, UnsupportedProviderException {
|
||||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||||
if (!userOpt.isPresent()) {
|
if (userOpt.isEmpty()) {
|
||||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||||
}
|
}
|
||||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||||
@@ -293,20 +295,20 @@ public class UserController {
|
|||||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
String userNameP = "";
|
String userNameP = "";
|
||||||
for (Object principal : principals) {
|
for (Object principal : principals) {
|
||||||
List<SessionInformation> sessionsInformations =
|
List<SessionInformation> sessionsInformation =
|
||||||
sessionRegistry.getAllSessions(principal, false);
|
sessionRegistry.getAllSessions(principal, false);
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
userNameP = ((UserDetails) principal).getUsername();
|
userNameP = detailsUser.getUsername();
|
||||||
} else if (principal instanceof OAuth2User) {
|
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||||
userNameP = ((OAuth2User) principal).getName();
|
userNameP = oAuth2User.getName();
|
||||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
userNameP = saml2User.name();
|
||||||
} else if (principal instanceof String) {
|
} else if (principal instanceof String stringUser) {
|
||||||
userNameP = (String) principal;
|
userNameP = stringUser;
|
||||||
}
|
}
|
||||||
if (userNameP.equalsIgnoreCase(username)) {
|
if (userNameP.equalsIgnoreCase(username)) {
|
||||||
for (SessionInformation sessionsInformation : sessionsInformations) {
|
for (SessionInformation sessionInfo : sessionsInformation) {
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
|
||||||
import stirling.software.SPDF.utils.FileToPdf;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
|
||||||
|
|
||||||
// @RestController
|
|
||||||
// @Tag(name = "Convert", description = "Convert APIs")
|
|
||||||
// @RequestMapping("/api/v1/convert")
|
|
||||||
public class ConvertBookToPDFController {
|
|
||||||
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public ConvertBookToPDFController(
|
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/book/pdf")
|
|
||||||
@Operation(
|
|
||||||
summary =
|
|
||||||
"Convert a BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) to PDF",
|
|
||||||
description =
|
|
||||||
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint takes an BOOK/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx) input and converts it to PDF format.")
|
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception {
|
|
||||||
MultipartFile fileInput = request.getFileInput();
|
|
||||||
|
|
||||||
if (!bookAndHtmlFormatsInstalled) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileInput == null) {
|
|
||||||
throw new IllegalArgumentException("Please provide a file for conversion.");
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
|
|
||||||
|
|
||||||
if (originalFilename != null) {
|
|
||||||
String originalFilenameLower = originalFilename.toLowerCase();
|
|
||||||
if (!originalFilenameLower.endsWith(".epub")
|
|
||||||
&& !originalFilenameLower.endsWith(".mobi")
|
|
||||||
&& !originalFilenameLower.endsWith(".azw3")
|
|
||||||
&& !originalFilenameLower.endsWith(".fb2")
|
|
||||||
&& !originalFilenameLower.endsWith(".txt")
|
|
||||||
&& !originalFilenameLower.endsWith(".docx")) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"File must be in .epub, .mobi, .azw3, .fb2, .txt, or .docx format.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
byte[] pdfBytes = FileToPdf.convertBookTypeToPdf(fileInput.getBytes(), originalFilename);
|
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
|
||||||
|
|
||||||
String outputFilename =
|
|
||||||
originalFilename.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ ".pdf"; // Remove file extension and append .pdf
|
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -13,6 +12,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.RuntimePathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
@@ -24,20 +24,21 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertHtmlToPDF {
|
public class ConvertHtmlToPDF {
|
||||||
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertHtmlToPDF(
|
public ConvertHtmlToPDF(
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled,
|
ApplicationProperties applicationProperties,
|
||||||
ApplicationProperties applicationProperties) {
|
RuntimePathConfig runtimePathConfig) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/html/pdf")
|
||||||
@@ -65,10 +66,10 @@ public class ConvertHtmlToPDF {
|
|||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
|
runtimePathConfig.getWeasyPrintPath(),
|
||||||
request,
|
request,
|
||||||
fileInput.getBytes(),
|
fileInput.getBytes(),
|
||||||
originalFilename,
|
originalFilename,
|
||||||
bookAndHtmlFormatsInstalled,
|
|
||||||
disableSanitize);
|
disableSanitize);
|
||||||
|
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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;
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import org.commonmark.parser.Parser;
|
|||||||
import org.commonmark.renderer.html.AttributeProvider;
|
import org.commonmark.renderer.html.AttributeProvider;
|
||||||
import org.commonmark.renderer.html.HtmlRenderer;
|
import org.commonmark.renderer.html.HtmlRenderer;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -23,6 +22,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.RuntimePathConfig;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
@@ -34,20 +34,20 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
public class ConvertMarkdownToPdf {
|
public class ConvertMarkdownToPdf {
|
||||||
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertMarkdownToPdf(
|
public ConvertMarkdownToPdf(
|
||||||
CustomPDDocumentFactory pdfDocumentFactory,
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled,
|
ApplicationProperties applicationProperties,
|
||||||
ApplicationProperties applicationProperties) {
|
RuntimePathConfig runtimePathConfig) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf")
|
||||||
@@ -86,10 +86,10 @@ public class ConvertMarkdownToPdf {
|
|||||||
|
|
||||||
byte[] pdfBytes =
|
byte[] pdfBytes =
|
||||||
FileToPdf.convertHtmlToPdf(
|
FileToPdf.convertHtmlToPdf(
|
||||||
|
runtimePathConfig.getWeasyPrintPath(),
|
||||||
null,
|
null,
|
||||||
htmlContent.getBytes(),
|
htmlContent.getBytes(),
|
||||||
"converted.html",
|
"converted.html",
|
||||||
bookAndHtmlFormatsInstalled,
|
|
||||||
disableSanitize);
|
disableSanitize);
|
||||||
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.RuntimePathConfig;
|
||||||
import stirling.software.SPDF.model.api.GeneralFile;
|
import stirling.software.SPDF.model.api.GeneralFile;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
@@ -34,10 +35,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertOfficeController {
|
public class ConvertOfficeController {
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
|
public ConvertOfficeController(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
|
||||||
@@ -61,7 +65,7 @@ public class ConvertOfficeController {
|
|||||||
List<String> command =
|
List<String> command =
|
||||||
new ArrayList<>(
|
new ArrayList<>(
|
||||||
Arrays.asList(
|
Arrays.asList(
|
||||||
"/opt/venv/bin/unoconvert",
|
runtimePathConfig.getUnoConvertPath(),
|
||||||
"--port",
|
"--port",
|
||||||
"2003",
|
"2003",
|
||||||
"--convert-to",
|
"--convert-to",
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
|
||||||
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
|
||||||
|
|
||||||
import io.github.pixee.security.Filenames;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToBookRequest;
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
|
||||||
|
|
||||||
// @RestController
|
|
||||||
// @Tag(name = "Convert", description = "Convert APIs")
|
|
||||||
// @RequestMapping("/api/v1/convert")
|
|
||||||
public class ConvertPDFToBookController {
|
|
||||||
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled")
|
|
||||||
private final boolean bookAndHtmlFormatsInstalled;
|
|
||||||
|
|
||||||
public ConvertPDFToBookController(
|
|
||||||
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
|
|
||||||
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
|
|
||||||
@Operation(
|
|
||||||
summary =
|
|
||||||
"Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF",
|
|
||||||
description =
|
|
||||||
"(Requires bookAndHtmlFormatsInstalled flag and Calibre installed) This endpoint Convert a PDF to a Book/comic (*.epub | *.mobi | *.azw3 | *.fb2 | *.txt | *.docx .. (others to include by chatgpt) to PDF")
|
|
||||||
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
|
|
||||||
throws Exception {
|
|
||||||
MultipartFile fileInput = request.getFileInput();
|
|
||||||
if (!bookAndHtmlFormatsInstalled) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
|
|
||||||
}
|
|
||||||
if (fileInput == null) {
|
|
||||||
throw new IllegalArgumentException("Please provide a file for conversion.");
|
|
||||||
}
|
|
||||||
// Validate the output format
|
|
||||||
String outputFormat = request.getOutputFormat().toLowerCase();
|
|
||||||
List<String> allowedFormats =
|
|
||||||
Arrays.asList(
|
|
||||||
"epub", "mobi", "azw3", "docx", "rtf", "txt", "html", "lit", "fb2", "pdb",
|
|
||||||
"lrf");
|
|
||||||
if (!allowedFormats.contains(outputFormat)) {
|
|
||||||
throw new IllegalArgumentException("Invalid output format: " + outputFormat);
|
|
||||||
}
|
|
||||||
byte[] outputFileBytes;
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
Path tempOutputFile =
|
|
||||||
Files.createTempFile(
|
|
||||||
"output_", // Use the output format for the file extension
|
|
||||||
"." + outputFormat);
|
|
||||||
Path tempInputFile = null;
|
|
||||||
try {
|
|
||||||
// Create temp input file from the provided PDF
|
|
||||||
// Assuming input is always PDF
|
|
||||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
|
||||||
Files.write(tempInputFile, fileInput.getBytes());
|
|
||||||
command.add("ebook-convert");
|
|
||||||
command.add(tempInputFile.toString());
|
|
||||||
command.add(tempOutputFile.toString());
|
|
||||||
ProcessExecutorResult returnCode =
|
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
|
||||||
.runCommandWithOutputHandling(command);
|
|
||||||
outputFileBytes = Files.readAllBytes(tempOutputFile);
|
|
||||||
} finally {
|
|
||||||
// Clean up temporary files
|
|
||||||
if (tempInputFile != null) {
|
|
||||||
Files.deleteIfExists(tempInputFile);
|
|
||||||
}
|
|
||||||
Files.deleteIfExists(tempOutputFile);
|
|
||||||
}
|
|
||||||
String outputFilename =
|
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "")
|
|
||||||
+ "."
|
|
||||||
+ // Remove file extension and append .pdf
|
|
||||||
outputFormat;
|
|
||||||
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
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.RuntimePathConfig;
|
||||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
@@ -32,10 +34,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
|
|||||||
public class ConvertWebsiteToPDF {
|
public class ConvertWebsiteToPDF {
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
private final RuntimePathConfig runtimePathConfig;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ConvertWebsiteToPDF(CustomPDDocumentFactory pdfDocumentFactory) {
|
public ConvertWebsiteToPDF(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
|
||||||
@@ -65,7 +70,7 @@ public class ConvertWebsiteToPDF {
|
|||||||
|
|
||||||
// Prepare the WeasyPrint command
|
// Prepare the WeasyPrint command
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("/opt/venv/bin/weasyprint");
|
command.add(runtimePathConfig.getWeasyPrintPath());
|
||||||
command.add(URL);
|
command.add(URL);
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||||
|
|
||||||
import technology.tabula.ObjectExtractor;
|
import technology.tabula.ObjectExtractor;
|
||||||
import technology.tabula.Page;
|
import technology.tabula.Page;
|
||||||
import technology.tabula.Table;
|
import technology.tabula.Table;
|
||||||
@@ -51,22 +54,20 @@ public class ExtractCSVController {
|
|||||||
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||||
List<Integer> pages = form.getPageNumbersList(document, true);
|
List<Integer> pages = form.getPageNumbersList(document, true);
|
||||||
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
||||||
CSVFormat format = CSVFormat.EXCEL.builder()
|
CSVFormat format =
|
||||||
.setEscape('"')
|
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
||||||
.setQuoteMode(QuoteMode.ALL)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
for (int pageNum : pages) {
|
for (int pageNum : pages) {
|
||||||
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
||||||
log.info("{}",pageNum);
|
log.info("{}", pageNum);
|
||||||
Page page = extractor.extract(pageNum);
|
Page page = extractor.extract(pageNum);
|
||||||
List<Table> tables = sea.extract(page);
|
List<Table> tables = sea.extract(page);
|
||||||
|
|
||||||
for (int i = 0; i < tables.size(); i++) {
|
for (int i = 0; i < tables.size(); i++) {
|
||||||
StringWriter sw = new StringWriter();
|
StringWriter sw = new StringWriter();
|
||||||
FlexibleCSVWriter csvWriter = new FlexibleCSVWriter(format);
|
FlexibleCSVWriter csvWriter = new FlexibleCSVWriter(format);
|
||||||
csvWriter.write(sw, Collections.singletonList(tables.get(i)));
|
csvWriter.write(sw, Collections.singletonList(tables.get(i)));
|
||||||
|
|
||||||
String entryName = generateEntryName(baseName, pageNum, i + 1);
|
String entryName = generateEntryName(baseName, pageNum, i + 1);
|
||||||
csvEntries.add(new CsvEntry(entryName, sw.toString()));
|
csvEntries.add(new CsvEntry(entryName, sw.toString()));
|
||||||
}
|
}
|
||||||
@@ -83,7 +84,8 @@ public class ExtractCSVController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<byte[]> createZipResponse(List<CsvEntry> entries, String baseName) throws IOException {
|
private ResponseEntity<byte[]> createZipResponse(List<CsvEntry> entries, String baseName)
|
||||||
|
throws IOException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
|
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
|
||||||
for (CsvEntry entry : entries) {
|
for (CsvEntry entry : entries) {
|
||||||
@@ -93,21 +95,25 @@ public class ExtractCSVController {
|
|||||||
zipOut.closeEntry();
|
zipOut.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentDisposition(ContentDisposition.builder("attachment")
|
headers.setContentDisposition(
|
||||||
.filename(baseName + "_extracted.zip").build());
|
ContentDisposition.builder("attachment")
|
||||||
|
.filename(baseName + "_extracted.zip")
|
||||||
|
.build());
|
||||||
headers.setContentType(MediaType.parseMediaType("application/zip"));
|
headers.setContentType(MediaType.parseMediaType("application/zip"));
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(baos.toByteArray());
|
return ResponseEntity.ok().headers(headers).body(baos.toByteArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<String> createCsvResponse(CsvEntry entry, String baseName) {
|
private ResponseEntity<String> createCsvResponse(CsvEntry entry, String baseName) {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setContentDisposition(ContentDisposition.builder("attachment")
|
headers.setContentDisposition(
|
||||||
.filename(baseName + "_extracted.csv").build());
|
ContentDisposition.builder("attachment")
|
||||||
|
.filename(baseName + "_extracted.csv")
|
||||||
|
.build());
|
||||||
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(entry.content());
|
return ResponseEntity.ok().headers(headers).body(entry.content());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -60,8 +61,8 @@ public class AutoSplitPdfController {
|
|||||||
private static String decodeQRCode(BufferedImage bufferedImage) {
|
private static String decodeQRCode(BufferedImage bufferedImage) {
|
||||||
LuminanceSource source;
|
LuminanceSource source;
|
||||||
|
|
||||||
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
|
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte dataBufferByte) {
|
||||||
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
|
byte[] pixels = dataBufferByte.getData();
|
||||||
source =
|
source =
|
||||||
new PlanarYUVLuminanceSource(
|
new PlanarYUVLuminanceSource(
|
||||||
pixels,
|
pixels,
|
||||||
@@ -72,8 +73,9 @@ public class AutoSplitPdfController {
|
|||||||
bufferedImage.getWidth(),
|
bufferedImage.getWidth(),
|
||||||
bufferedImage.getHeight(),
|
bufferedImage.getHeight(),
|
||||||
false);
|
false);
|
||||||
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
|
} else if (bufferedImage.getRaster().getDataBuffer()
|
||||||
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
|
instanceof DataBufferInt dataBufferInt) {
|
||||||
|
int[] pixels = dataBufferInt.getData();
|
||||||
byte[] newPixels = new byte[pixels.length];
|
byte[] newPixels = new byte[pixels.length];
|
||||||
for (int i = 0; i < pixels.length; i++) {
|
for (int i = 0; i < pixels.length; i++) {
|
||||||
newPixels[i] = (byte) (pixels[i] & 0xff);
|
newPixels[i] = (byte) (pixels[i] & 0xff);
|
||||||
@@ -90,7 +92,8 @@ public class AutoSplitPdfController {
|
|||||||
false);
|
false);
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
|
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed"
|
||||||
|
+ " int), byte gray, or 3-byte/4-byte RGB image data");
|
||||||
}
|
}
|
||||||
|
|
||||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||||
@@ -107,7 +110,10 @@ public class AutoSplitPdfController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Auto split PDF pages into separate documents",
|
summary = "Auto split PDF pages into separate documents",
|
||||||
description =
|
description =
|
||||||
"This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
|
"This endpoint accepts a PDF file, scans each page for a specific QR code, and"
|
||||||
|
+ " splits the document at the QR code boundaries. The output is a zip file"
|
||||||
|
+ " containing each separate PDF document. Input:PDF Output:ZIP-PDF"
|
||||||
|
+ " Type:SISO")
|
||||||
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
|
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.GeneralUtils;
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
@@ -51,7 +52,8 @@ public class CompressController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor) throws Exception {
|
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor, boolean grayScale)
|
||||||
|
throws Exception {
|
||||||
byte[] fileBytes = Files.readAllBytes(pdfFile);
|
byte[] fileBytes = Files.readAllBytes(pdfFile);
|
||||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||||
double scaleFactor = initialScaleFactor;
|
double scaleFactor = initialScaleFactor;
|
||||||
@@ -61,8 +63,7 @@ public class CompressController {
|
|||||||
if (res != null && res.getXObjectNames() != null) {
|
if (res != null && res.getXObjectNames() != null) {
|
||||||
for (COSName name : res.getXObjectNames()) {
|
for (COSName name : res.getXObjectNames()) {
|
||||||
PDXObject xobj = res.getXObject(name);
|
PDXObject xobj = res.getXObject(name);
|
||||||
if (xobj instanceof PDImageXObject) {
|
if (xobj instanceof PDImageXObject image) {
|
||||||
PDImageXObject image = (PDImageXObject) xobj;
|
|
||||||
BufferedImage bufferedImage = image.getImage();
|
BufferedImage bufferedImage = image.getImage();
|
||||||
|
|
||||||
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
|
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
|
||||||
@@ -76,11 +77,23 @@ public class CompressController {
|
|||||||
bufferedImage.getScaledInstance(
|
bufferedImage.getScaledInstance(
|
||||||
newWidth, newHeight, Image.SCALE_SMOOTH);
|
newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||||
|
|
||||||
BufferedImage scaledBufferedImage =
|
BufferedImage scaledBufferedImage;
|
||||||
new BufferedImage(
|
if (grayScale
|
||||||
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
|| bufferedImage.getType() == BufferedImage.TYPE_BYTE_GRAY) {
|
||||||
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
scaledBufferedImage =
|
||||||
|
new BufferedImage(
|
||||||
|
newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
|
||||||
|
scaledBufferedImage
|
||||||
|
.getGraphics()
|
||||||
|
.drawImage(scaledImage, 0, 0, null);
|
||||||
|
} else {
|
||||||
|
scaledBufferedImage =
|
||||||
|
new BufferedImage(
|
||||||
|
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||||
|
scaledBufferedImage
|
||||||
|
.getGraphics()
|
||||||
|
.drawImage(scaledImage, 0, 0, null);
|
||||||
|
}
|
||||||
ByteArrayOutputStream compressedImageStream =
|
ByteArrayOutputStream compressedImageStream =
|
||||||
new ByteArrayOutputStream();
|
new ByteArrayOutputStream();
|
||||||
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
||||||
@@ -105,7 +118,8 @@ public class CompressController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Optimize PDF file",
|
summary = "Optimize PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO")
|
"This endpoint accepts a PDF file and optimizes it based on the provided"
|
||||||
|
+ " parameters. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
|
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
@@ -139,6 +153,7 @@ public class CompressController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean sizeMet = false;
|
boolean sizeMet = false;
|
||||||
|
boolean grayscaleEnabled = Boolean.TRUE.equals(request.getGrayscale());
|
||||||
while (!sizeMet && optimizeLevel <= 9) {
|
while (!sizeMet && optimizeLevel <= 9) {
|
||||||
|
|
||||||
// Apply additional image compression for levels 6-9
|
// Apply additional image compression for levels 6-9
|
||||||
@@ -152,7 +167,7 @@ public class CompressController {
|
|||||||
case 9 -> 0.5; // 60% of original size
|
case 9 -> 0.5; // 60% of original size
|
||||||
default -> 1.0;
|
default -> 1.0;
|
||||||
};
|
};
|
||||||
compressImagesInPDF(tempInputFile, scaleFactor);
|
compressImagesInPDF(tempInputFile, scaleFactor, grayscaleEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run QPDF optimization
|
// Run QPDF optimization
|
||||||
@@ -169,6 +184,7 @@ public class CompressController {
|
|||||||
command.add("--compression-level=" + optimizeLevel);
|
command.add("--compression-level=" + optimizeLevel);
|
||||||
command.add("--compress-streams=y");
|
command.add("--compress-streams=y");
|
||||||
command.add("--object-streams=generate");
|
command.add("--object-streams=generate");
|
||||||
|
command.add("--no-warn");
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
command.add(tempOutputFile.toString());
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
@@ -205,7 +221,8 @@ public class CompressController {
|
|||||||
// Check if optimized file is larger than the original
|
// Check if optimized file is larger than the original
|
||||||
if (pdfBytes.length > inputFileSize) {
|
if (pdfBytes.length > inputFileSize) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Optimized file is larger than the original. Returning the original file instead.");
|
"Optimized file is larger than the original. Returning the original file"
|
||||||
|
+ " instead.");
|
||||||
finalFile = tempInputFile;
|
finalFile = tempInputFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||||
import stirling.software.SPDF.utils.CheckProgramInstall;
|
import stirling.software.SPDF.utils.CheckProgramInstall;
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||||
import stirling.software.SPDF.utils.ImageProcessingUtils;
|
import stirling.software.SPDF.utils.ImageProcessingUtils;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
|
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.PdfUtils;
|
import stirling.software.SPDF.utils.PdfUtils;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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;
|
||||||
|
|||||||
@@ -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.PipelineConfig;
|
import stirling.software.SPDF.model.PipelineConfig;
|
||||||
import stirling.software.SPDF.model.PipelineResult;
|
import stirling.software.SPDF.model.PipelineResult;
|
||||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.FileSystemException;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
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;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
@@ -24,7 +29,8 @@ 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.config.RuntimePathConfig;
|
||||||
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.PipelineResult;
|
import stirling.software.SPDF.model.PipelineResult;
|
||||||
@@ -50,18 +56,19 @@ public class PipelineDirectoryProcessor {
|
|||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
ApiDocService apiDocService,
|
ApiDocService apiDocService,
|
||||||
PipelineProcessor processor,
|
PipelineProcessor processor,
|
||||||
FileMonitor fileMonitor) {
|
FileMonitor fileMonitor,
|
||||||
|
RuntimePathConfig runtimePathConfig) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.apiDocService = apiDocService;
|
this.apiDocService = apiDocService;
|
||||||
this.watchedFoldersDir = InstallationPathConfig.getPipelineWatchedFoldersDir();
|
this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath();
|
||||||
this.finishedFoldersDir = InstallationPathConfig.getPipelineFinishedFoldersDir();
|
this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath();
|
||||||
this.processor = processor;
|
this.processor = processor;
|
||||||
this.fileMonitor = fileMonitor;
|
this.fileMonitor = fileMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedRate = 60000)
|
@Scheduled(fixedRate = 60000)
|
||||||
public void scanFolders() {
|
public void scanFolders() {
|
||||||
Path watchedFolderPath = Paths.get(watchedFoldersDir);
|
Path watchedFolderPath = Paths.get(watchedFoldersDir).toAbsolutePath();
|
||||||
if (!Files.exists(watchedFolderPath)) {
|
if (!Files.exists(watchedFolderPath)) {
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(watchedFolderPath);
|
Files.createDirectories(watchedFolderPath);
|
||||||
@@ -71,19 +78,33 @@ public class PipelineDirectoryProcessor {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
|
|
||||||
paths.filter(Files::isDirectory)
|
try {
|
||||||
.forEach(
|
Files.walkFileTree(
|
||||||
t -> {
|
watchedFolderPath,
|
||||||
try {
|
new SimpleFileVisitor<>() {
|
||||||
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
|
@Override
|
||||||
handleDirectory(t);
|
public FileVisitResult preVisitDirectory(
|
||||||
}
|
Path dir, BasicFileAttributes attrs) {
|
||||||
} catch (Exception e) {
|
try {
|
||||||
log.error("Error handling directory: {}", t, e);
|
// Skip root directory and "processing" subdirectories
|
||||||
|
if (!dir.equals(watchedFolderPath) && !dir.endsWith("processing")) {
|
||||||
|
handleDirectory(dir);
|
||||||
}
|
}
|
||||||
});
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
log.error("Error handling directory: {}", dir, e);
|
||||||
|
}
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFileFailed(Path path, IOException exc) {
|
||||||
|
// Handle broken symlinks or inaccessible directories
|
||||||
|
log.error("Error accessing path: {}", path, exc);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
log.error("Error walking through directory: {}", watchedFolderPath, e);
|
log.error("Error walking through directory: {}", watchedFolderPath, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,6 +208,7 @@ public class PipelineDirectoryProcessor {
|
|||||||
}
|
}
|
||||||
return isAllowed;
|
return isAllowed;
|
||||||
})
|
})
|
||||||
|
.map(Path::toAbsolutePath)
|
||||||
.filter(
|
.filter(
|
||||||
path -> {
|
path -> {
|
||||||
boolean isReady =
|
boolean isReady =
|
||||||
@@ -200,7 +222,10 @@ public class PipelineDirectoryProcessor {
|
|||||||
})
|
})
|
||||||
.map(Path::toFile)
|
.map(Path::toFile)
|
||||||
.toArray(File[]::new);
|
.toArray(File[]::new);
|
||||||
log.info("Collected {} files for processing", files.length);
|
log.info(
|
||||||
|
"Collected {} files for processing for {}",
|
||||||
|
files.length,
|
||||||
|
dir.toAbsolutePath().toString());
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,8 +235,35 @@ public class PipelineDirectoryProcessor {
|
|||||||
List<File> filesToProcess = new ArrayList<>();
|
List<File> filesToProcess = new ArrayList<>();
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
|
Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
|
||||||
Files.move(file.toPath(), targetPath);
|
|
||||||
filesToProcess.add(targetPath.toFile());
|
// Retry with exponential backoff
|
||||||
|
int maxRetries = 3;
|
||||||
|
int retryDelayMs = 500;
|
||||||
|
boolean moved = false;
|
||||||
|
|
||||||
|
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
Files.move(file.toPath(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
moved = true;
|
||||||
|
break;
|
||||||
|
} catch (FileSystemException e) {
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
log.info("File move failed (attempt {}), retrying...", attempt);
|
||||||
|
try {
|
||||||
|
Thread.sleep(retryDelayMs * (int) Math.pow(2, attempt - 1));
|
||||||
|
} catch (InterruptedException e1) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e1.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moved) {
|
||||||
|
filesToProcess.add(targetPath.toFile());
|
||||||
|
} else {
|
||||||
|
log.error("Failed to move file after {} attempts: {}", maxRetries, file.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return filesToProcess;
|
return filesToProcess;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,9 @@ import io.github.pixee.security.Filenames;
|
|||||||
import io.github.pixee.security.ZipSecurity;
|
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;
|
||||||
@@ -116,9 +118,8 @@ public class PipelineProcessor {
|
|||||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
if (entry.getValue() instanceof List) {
|
if (entry.getValue() instanceof List<?> entryList) {
|
||||||
List<?> list = (List<?>) entry.getValue();
|
for (Object item : entryList) {
|
||||||
for (Object item : list) {
|
|
||||||
body.add(entry.getKey(), item);
|
body.add(entry.getKey(), item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -137,7 +138,7 @@ public class PipelineProcessor {
|
|||||||
log.info("Skipping file due to filtering {}", operation);
|
log.info("Skipping file due to filtering {}", operation);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!response.getStatusCode().equals(HttpStatus.OK)) {
|
if (!HttpStatus.OK.equals(response.getStatusCode())) {
|
||||||
logPrintStream.println("Error: " + response.getBody());
|
logPrintStream.println("Error: " + response.getBody());
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -178,9 +179,8 @@ public class PipelineProcessor {
|
|||||||
body.add("fileInput", file);
|
body.add("fileInput", file);
|
||||||
}
|
}
|
||||||
for (Entry<String, Object> entry : parameters.entrySet()) {
|
for (Entry<String, Object> entry : parameters.entrySet()) {
|
||||||
if (entry.getValue() instanceof List) {
|
if (entry.getValue() instanceof List<?> entryList) {
|
||||||
List<?> list = (List<?>) entry.getValue();
|
for (Object item : entryList) {
|
||||||
for (Object item : list) {
|
|
||||||
body.add(entry.getKey(), item);
|
body.add(entry.getKey(), item);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -189,7 +189,7 @@ public class PipelineProcessor {
|
|||||||
}
|
}
|
||||||
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
ResponseEntity<byte[]> response = sendWebRequest(url, body);
|
||||||
// Handle the response
|
// Handle the response
|
||||||
if (response.getStatusCode().equals(HttpStatus.OK)) {
|
if (HttpStatus.OK.equals(response.getStatusCode())) {
|
||||||
processOutputFiles(operation, response, newOutputFiles);
|
processOutputFiles(operation, response, newOutputFiles);
|
||||||
} else {
|
} else {
|
||||||
// Log error if the response status is not OK
|
// Log error if the response status is not OK
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
@@ -128,9 +129,9 @@ public class CertSignController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sign PDF with a Digital Certificate",
|
summary = "Sign PDF with a Digital Certificate",
|
||||||
description =
|
description =
|
||||||
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
|
"This endpoint accepts a PDF file, a digital certificate and related"
|
||||||
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
|
+ " information to sign the PDF. It then returns the digitally signed PDF"
|
||||||
+ " Type:SISO")
|
+ " file. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile pdf = request.getFileInput();
|
MultipartFile pdf = request.getFileInput();
|
||||||
@@ -200,17 +201,14 @@ public class CertSignController {
|
|||||||
Object pemObject = pemParser.readObject();
|
Object pemObject = pemParser.readObject();
|
||||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
|
||||||
PrivateKeyInfo pkInfo;
|
PrivateKeyInfo pkInfo;
|
||||||
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
|
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo pkcs8EncryptedPrivateKeyInfo) {
|
||||||
InputDecryptorProvider decProv =
|
InputDecryptorProvider decProv =
|
||||||
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
|
||||||
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
|
pkInfo = pkcs8EncryptedPrivateKeyInfo.decryptPrivateKeyInfo(decProv);
|
||||||
} else if (pemObject instanceof PEMEncryptedKeyPair) {
|
} else if (pemObject instanceof PEMEncryptedKeyPair pemEncryptedKeyPair) {
|
||||||
PEMDecryptorProvider decProv =
|
PEMDecryptorProvider decProv =
|
||||||
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
|
||||||
pkInfo =
|
pkInfo = pemEncryptedKeyPair.decryptKeyPair(decProv).getPrivateKeyInfo();
|
||||||
((PEMEncryptedKeyPair) pemObject)
|
|
||||||
.decryptKeyPair(decProv)
|
|
||||||
.getPrivateKeyInfo();
|
|
||||||
} else {
|
} else {
|
||||||
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@@ -213,10 +214,7 @@ public class GetInfoOnPDF {
|
|||||||
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
ArrayNode attachmentsArray = objectMapper.createArrayNode();
|
||||||
for (PDPage page : pdfBoxDoc.getPages()) {
|
for (PDPage page : pdfBoxDoc.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation instanceof PDAnnotationFileAttachment) {
|
if (annotation instanceof PDAnnotationFileAttachment fileAttachmentAnnotation) {
|
||||||
PDAnnotationFileAttachment fileAttachmentAnnotation =
|
|
||||||
(PDAnnotationFileAttachment) annotation;
|
|
||||||
|
|
||||||
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
ObjectNode attachmentNode = objectMapper.createObjectNode();
|
||||||
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName());
|
||||||
attachmentNode.put("Description", fileAttachmentAnnotation.getContents());
|
attachmentNode.put("Description", fileAttachmentAnnotation.getContents());
|
||||||
@@ -436,9 +434,7 @@ public class GetInfoOnPDF {
|
|||||||
|
|
||||||
for (COSName name : resources.getXObjectNames()) {
|
for (COSName name : resources.getXObjectNames()) {
|
||||||
PDXObject xObject = resources.getXObject(name);
|
PDXObject xObject = resources.getXObject(name);
|
||||||
if (xObject instanceof PDImageXObject) {
|
if (xObject instanceof PDImageXObject image) {
|
||||||
PDImageXObject image = (PDImageXObject) xObject;
|
|
||||||
|
|
||||||
ObjectNode imageNode = objectMapper.createObjectNode();
|
ObjectNode imageNode = objectMapper.createObjectNode();
|
||||||
imageNode.put("Width", image.getWidth());
|
imageNode.put("Width", image.getWidth());
|
||||||
imageNode.put("Height", image.getHeight());
|
imageNode.put("Height", image.getHeight());
|
||||||
@@ -461,10 +457,8 @@ public class GetInfoOnPDF {
|
|||||||
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
Set<String> uniqueURIs = new HashSet<>(); // To store unique URIs
|
||||||
|
|
||||||
for (PDAnnotation annotation : annotations) {
|
for (PDAnnotation annotation : annotations) {
|
||||||
if (annotation instanceof PDAnnotationLink) {
|
if (annotation instanceof PDAnnotationLink linkAnnotation) {
|
||||||
PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation;
|
if (linkAnnotation.getAction() instanceof PDActionURI uriAction) {
|
||||||
if (linkAnnotation.getAction() instanceof PDActionURI) {
|
|
||||||
PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction();
|
|
||||||
String uri = uriAction.getURI();
|
String uri = uriAction.getURI();
|
||||||
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
uniqueURIs.add(uri); // Add to set to ensure uniqueness
|
||||||
}
|
}
|
||||||
@@ -540,8 +534,7 @@ public class GetInfoOnPDF {
|
|||||||
Iterable<COSName> colorSpaceNames = resources.getColorSpaceNames();
|
Iterable<COSName> colorSpaceNames = resources.getColorSpaceNames();
|
||||||
for (COSName name : colorSpaceNames) {
|
for (COSName name : colorSpaceNames) {
|
||||||
PDColorSpace colorSpace = resources.getColorSpace(name);
|
PDColorSpace colorSpace = resources.getColorSpace(name);
|
||||||
if (colorSpace instanceof PDICCBased) {
|
if (colorSpace instanceof PDICCBased iccBased) {
|
||||||
PDICCBased iccBased = (PDICCBased) colorSpace;
|
|
||||||
PDStream iccData = iccBased.getPDStream();
|
PDStream iccData = iccBased.getPDStream();
|
||||||
byte[] iccBytes = iccData.toByteArray();
|
byte[] iccBytes = iccData.toByteArray();
|
||||||
|
|
||||||
@@ -697,12 +690,10 @@ public class GetInfoOnPDF {
|
|||||||
ArrayNode elementsArray = objectMapper.createArrayNode();
|
ArrayNode elementsArray = objectMapper.createArrayNode();
|
||||||
if (nodes != null) {
|
if (nodes != null) {
|
||||||
for (Object obj : nodes) {
|
for (Object obj : nodes) {
|
||||||
if (obj instanceof PDStructureNode) {
|
if (obj instanceof PDStructureNode node) {
|
||||||
PDStructureNode node = (PDStructureNode) obj;
|
|
||||||
ObjectNode elementNode = objectMapper.createObjectNode();
|
ObjectNode elementNode = objectMapper.createObjectNode();
|
||||||
|
|
||||||
if (node instanceof PDStructureElement) {
|
if (node instanceof PDStructureElement structureElement) {
|
||||||
PDStructureElement structureElement = (PDStructureElement) node;
|
|
||||||
elementNode.put("Type", structureElement.getStructureType());
|
elementNode.put("Type", structureElement.getStructureType());
|
||||||
elementNode.put("Content", getContent(structureElement));
|
elementNode.put("Content", getContent(structureElement));
|
||||||
|
|
||||||
@@ -723,8 +714,7 @@ public class GetInfoOnPDF {
|
|||||||
StringBuilder contentBuilder = new StringBuilder();
|
StringBuilder contentBuilder = new StringBuilder();
|
||||||
|
|
||||||
for (Object item : structureElement.getKids()) {
|
for (Object item : structureElement.getKids()) {
|
||||||
if (item instanceof COSString) {
|
if (item instanceof COSString cosString) {
|
||||||
COSString cosString = (COSString) item;
|
|
||||||
contentBuilder.append(cosString.getString());
|
contentBuilder.append(cosString.getString());
|
||||||
} else if (item instanceof PDStructureElement) {
|
} else if (item instanceof PDStructureElement) {
|
||||||
// For simplicity, we're handling only COSString and PDStructureElement here
|
// For simplicity, we're handling only COSString and PDStructureElement here
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import 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.ManualRedactPdfRequest;
|
||||||
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
import stirling.software.SPDF.model.api.security.RedactPdfRequest;
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ public class SanitizeController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Sanitize a PDF file",
|
summary = "Sanitize a PDF file",
|
||||||
description =
|
description =
|
||||||
"This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO")
|
"This endpoint processes a PDF file and removes specific elements based on the"
|
||||||
|
+ " provided options. Input:PDF Output:PDF Type:SISO")
|
||||||
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
|
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
@@ -103,8 +104,7 @@ public class SanitizeController {
|
|||||||
|
|
||||||
for (PDPage page : document.getPages()) {
|
for (PDPage page : document.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation instanceof PDAnnotationWidget) {
|
if (annotation instanceof PDAnnotationWidget widget) {
|
||||||
PDAnnotationWidget widget = (PDAnnotationWidget) annotation;
|
|
||||||
PDAction action = widget.getAction();
|
PDAction action = widget.getAction();
|
||||||
if (action instanceof PDActionJavaScript) {
|
if (action instanceof PDActionJavaScript) {
|
||||||
widget.setAction(null);
|
widget.setAction(null);
|
||||||
@@ -157,12 +157,12 @@ public class SanitizeController {
|
|||||||
private void sanitizeLinks(PDDocument document) throws IOException {
|
private void sanitizeLinks(PDDocument document) throws IOException {
|
||||||
for (PDPage page : document.getPages()) {
|
for (PDPage page : document.getPages()) {
|
||||||
for (PDAnnotation annotation : page.getAnnotations()) {
|
for (PDAnnotation annotation : page.getAnnotations()) {
|
||||||
if (annotation != null && annotation instanceof PDAnnotationLink) {
|
if (annotation != null && annotation instanceof PDAnnotationLink linkAnnotation) {
|
||||||
PDAction action = ((PDAnnotationLink) annotation).getAction();
|
PDAction action = linkAnnotation.getAction();
|
||||||
if (action != null
|
if (action != null
|
||||||
&& (action instanceof PDActionLaunch
|
&& (action instanceof PDActionLaunch
|
||||||
|| action instanceof PDActionURI)) {
|
|| action instanceof PDActionURI)) {
|
||||||
((PDAnnotationLink) annotation).setAction(null);
|
linkAnnotation.setAction(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
package stirling.software.SPDF.controller.web;
|
package stirling.software.SPDF.controller.web;
|
||||||
|
|
||||||
|
import static stirling.software.SPDF.utils.validation.Validator.validateProvider;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -19,15 +26,21 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import 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.*;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
import stirling.software.SPDF.model.Authority;
|
||||||
|
import stirling.software.SPDF.model.Role;
|
||||||
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
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.GoogleProvider;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
@@ -37,12 +50,12 @@ import stirling.software.SPDF.repository.UserRepository;
|
|||||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||||
public class AccountWebController {
|
public class AccountWebController {
|
||||||
|
|
||||||
|
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
// Assuming you have a repository for user operations
|
||||||
private final UserRepository // Assuming you have a repository for user operations
|
private final UserRepository userRepository;
|
||||||
userRepository;
|
|
||||||
|
|
||||||
public AccountWebController(
|
public AccountWebController(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@@ -59,132 +72,125 @@ public class AccountWebController {
|
|||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication != null && authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> providerList = new HashMap<>();
|
Map<String, String> providerList = new HashMap<>();
|
||||||
Security securityProps = applicationProperties.getSecurity();
|
Security securityProps = applicationProperties.getSecurity();
|
||||||
OAUTH2 oauth = securityProps.getOauth2();
|
OAUTH2 oauth = securityProps.getOauth2();
|
||||||
|
|
||||||
if (oauth != null) {
|
if (oauth != null) {
|
||||||
if (oauth.getEnabled()) {
|
if (oauth.getEnabled()) {
|
||||||
if (oauth.isSettingsValid()) {
|
if (oauth.isSettingsValid()) {
|
||||||
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
|
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
|
||||||
|
String clientName =
|
||||||
|
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
|
||||||
|
providerList.put(OAUTH_2_AUTHORIZATION + oauth.getProvider(), clientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
Client client = oauth.getClient();
|
Client client = oauth.getClient();
|
||||||
|
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
GoogleProvider google = client.getGoogle();
|
GoogleProvider google = client.getGoogle();
|
||||||
if (google.isSettingsValid()) {
|
|
||||||
|
if (validateProvider(google)) {
|
||||||
providerList.put(
|
providerList.put(
|
||||||
"/oauth2/authorization/" + google.getName(),
|
OAUTH_2_AUTHORIZATION + google.getName(), google.getClientName());
|
||||||
google.getClientName());
|
|
||||||
}
|
}
|
||||||
GithubProvider github = client.getGithub();
|
|
||||||
if (github.isSettingsValid()) {
|
GitHubProvider github = client.getGithub();
|
||||||
|
|
||||||
|
if (validateProvider(github)) {
|
||||||
providerList.put(
|
providerList.put(
|
||||||
"/oauth2/authorization/" + github.getName(),
|
OAUTH_2_AUTHORIZATION + github.getName(), github.getClientName());
|
||||||
github.getClientName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
KeycloakProvider keycloak = client.getKeycloak();
|
||||||
if (keycloak.isSettingsValid()) {
|
|
||||||
|
if (validateProvider(keycloak)) {
|
||||||
providerList.put(
|
providerList.put(
|
||||||
"/oauth2/authorization/" + keycloak.getName(),
|
OAUTH_2_AUTHORIZATION + keycloak.getName(),
|
||||||
keycloak.getClientName());
|
keycloak.getClientName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SAML2 saml2 = securityProps.getSaml2();
|
SAML2 saml2 = securityProps.getSaml2();
|
||||||
if (securityProps.isSaml2Activ()
|
|
||||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
if (securityProps.isSaml2Active()
|
||||||
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
|
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||||
|
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||||
|
String samlIdp = saml2.getProvider();
|
||||||
|
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||||
|
|
||||||
|
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
|
||||||
|
return "redirect:" + request.getRequestURL() + saml2AuthenticationPath;
|
||||||
|
} else {
|
||||||
|
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any null keys/values from the providerList
|
// Remove any null keys/values from the providerList
|
||||||
providerList
|
providerList
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||||
model.addAttribute("providerlist", providerList);
|
model.addAttribute("providerList", providerList);
|
||||||
model.addAttribute("loginMethod", securityProps.getLoginMethod());
|
model.addAttribute("loginMethod", securityProps.getLoginMethod());
|
||||||
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
|
|
||||||
|
boolean altLogin = !providerList.isEmpty() ? securityProps.isAltLogin() : false;
|
||||||
|
|
||||||
model.addAttribute("altLogin", altLogin);
|
model.addAttribute("altLogin", altLogin);
|
||||||
model.addAttribute("currentPage", "login");
|
model.addAttribute("currentPage", "login");
|
||||||
String error = request.getParameter("error");
|
String error = request.getParameter("error");
|
||||||
|
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
switch (error) {
|
switch (error) {
|
||||||
case "badcredentials":
|
case "badCredentials" -> error = "login.invalid";
|
||||||
error = "login.invalid";
|
case "locked" -> error = "login.locked";
|
||||||
break;
|
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
|
||||||
case "locked":
|
|
||||||
error = "login.locked";
|
|
||||||
break;
|
|
||||||
case "oauth2AuthenticationError":
|
|
||||||
error = "userAlreadyExistsOAuthMessage";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("error", error);
|
model.addAttribute("error", error);
|
||||||
}
|
}
|
||||||
String erroroauth = request.getParameter("erroroauth");
|
|
||||||
if (erroroauth != null) {
|
String errorOAuth = request.getParameter("errorOAuth");
|
||||||
switch (erroroauth) {
|
|
||||||
case "oauth2AutoCreateDisabled":
|
if (errorOAuth != null) {
|
||||||
erroroauth = "login.oauth2AutoCreateDisabled";
|
switch (errorOAuth) {
|
||||||
break;
|
case "oAuth2AutoCreateDisabled" -> errorOAuth = "login.oAuth2AutoCreateDisabled";
|
||||||
case "invalidUsername":
|
case "invalidUsername" -> errorOAuth = "login.invalid";
|
||||||
erroroauth = "login.invalid";
|
case "userAlreadyExistsWeb" -> errorOAuth = "userAlreadyExistsWebMessage";
|
||||||
break;
|
case "oAuth2AuthenticationErrorWeb" -> errorOAuth = "login.oauth2InvalidUserType";
|
||||||
case "userAlreadyExistsWeb":
|
case "invalid_token_response" -> errorOAuth = "login.oauth2InvalidTokenResponse";
|
||||||
erroroauth = "userAlreadyExistsWebMessage";
|
case "authorization_request_not_found" ->
|
||||||
break;
|
errorOAuth = "login.oauth2RequestNotFound";
|
||||||
case "oauth2AuthenticationErrorWeb":
|
case "access_denied" -> errorOAuth = "login.oauth2AccessDenied";
|
||||||
erroroauth = "login.oauth2InvalidUserType";
|
case "invalid_user_info_response" ->
|
||||||
break;
|
errorOAuth = "login.oauth2InvalidUserInfoResponse";
|
||||||
case "invalid_token_response":
|
case "invalid_request" -> errorOAuth = "login.oauth2invalidRequest";
|
||||||
erroroauth = "login.oauth2InvalidTokenResponse";
|
case "invalid_id_token" -> errorOAuth = "login.oauth2InvalidIdToken";
|
||||||
break;
|
case "oAuth2AdminBlockedUser" -> errorOAuth = "login.oAuth2AdminBlockedUser";
|
||||||
case "authorization_request_not_found":
|
case "userIsDisabled" -> errorOAuth = "login.userIsDisabled";
|
||||||
erroroauth = "login.oauth2RequestNotFound";
|
case "invalid_destination" -> errorOAuth = "login.invalid_destination";
|
||||||
break;
|
case "relying_party_registration_not_found" ->
|
||||||
case "access_denied":
|
errorOAuth = "login.relyingPartyRegistrationNotFound";
|
||||||
erroroauth = "login.oauth2AccessDenied";
|
|
||||||
break;
|
|
||||||
case "invalid_user_info_response":
|
|
||||||
erroroauth = "login.oauth2InvalidUserInfoResponse";
|
|
||||||
break;
|
|
||||||
case "invalid_request":
|
|
||||||
erroroauth = "login.oauth2invalidRequest";
|
|
||||||
break;
|
|
||||||
case "invalid_id_token":
|
|
||||||
erroroauth = "login.oauth2InvalidIdToken";
|
|
||||||
break;
|
|
||||||
case "oauth2_admin_blocked_user":
|
|
||||||
erroroauth = "login.oauth2AdminBlockedUser";
|
|
||||||
break;
|
|
||||||
case "userIsDisabled":
|
|
||||||
erroroauth = "login.userIsDisabled";
|
|
||||||
break;
|
|
||||||
case "invalid_destination":
|
|
||||||
erroroauth = "login.invalid_destination";
|
|
||||||
break;
|
|
||||||
case "relying_party_registration_not_found":
|
|
||||||
erroroauth = "login.relyingPartyRegistrationNotFound";
|
|
||||||
break;
|
|
||||||
// Valid InResponseTo was not available from the validation context, unable to
|
// Valid InResponseTo was not available from the validation context, unable to
|
||||||
// evaluate
|
// evaluate
|
||||||
case "invalid_in_response_to":
|
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to";
|
||||||
erroroauth = "login.invalid_in_response_to";
|
case "not_authentication_provider_found" ->
|
||||||
break;
|
errorOAuth = "login.not_authentication_provider_found";
|
||||||
case "not_authentication_provider_found":
|
|
||||||
erroroauth = "login.not_authentication_provider_found";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
model.addAttribute("erroroauth", erroroauth);
|
|
||||||
|
model.addAttribute("errorOAuth", errorOAuth);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.getParameter("messageType") != null) {
|
if (request.getParameter("messageType") != null) {
|
||||||
model.addAttribute("messageType", "changedCredsMessage");
|
model.addAttribute("messageType", "changedCredsMessage");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.getParameter("logout") != null) {
|
if (request.getParameter("logout") != null) {
|
||||||
model.addAttribute("logoutMessage", "You have been logged out.");
|
model.addAttribute("logoutMessage", "You have been logged out.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return "login";
|
return "login";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,13 +234,11 @@ public class AccountWebController {
|
|||||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
if (now.isAfter(expirationTime)) {
|
if (now.isAfter(expirationTime)) {
|
||||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||||
hasActiveSession = false;
|
|
||||||
} else {
|
} else {
|
||||||
hasActiveSession = !sessionEntity.isExpired();
|
hasActiveSession = !sessionEntity.isExpired();
|
||||||
}
|
}
|
||||||
lastRequest = sessionEntity.getLastRequest();
|
lastRequest = sessionEntity.getLastRequest();
|
||||||
} else {
|
} else {
|
||||||
hasActiveSession = false;
|
|
||||||
// No session, set default last request time
|
// No session, set default last request time
|
||||||
lastRequest = new Date(0);
|
lastRequest = new Date(0);
|
||||||
}
|
}
|
||||||
@@ -271,53 +275,41 @@ public class AccountWebController {
|
|||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
String deleteMessage = null;
|
|
||||||
|
String deleteMessage;
|
||||||
if (messageType != null) {
|
if (messageType != null) {
|
||||||
switch (messageType) {
|
deleteMessage =
|
||||||
case "deleteCurrentUser":
|
switch (messageType) {
|
||||||
deleteMessage = "deleteCurrentUserMessage";
|
case "deleteCurrentUser" -> "deleteCurrentUserMessage";
|
||||||
break;
|
case "deleteUsernameExists" -> "deleteUsernameExistsMessage";
|
||||||
case "deleteUsernameExists":
|
default -> null;
|
||||||
deleteMessage = "deleteUsernameExistsMessage";
|
};
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
model.addAttribute("deleteMessage", deleteMessage);
|
model.addAttribute("deleteMessage", deleteMessage);
|
||||||
String addMessage = null;
|
|
||||||
switch (messageType) {
|
String addMessage;
|
||||||
case "usernameExists":
|
addMessage =
|
||||||
addMessage = "usernameExistsMessage";
|
switch (messageType) {
|
||||||
break;
|
case "usernameExists" -> "usernameExistsMessage";
|
||||||
case "invalidUsername":
|
case "invalidUsername" -> "invalidUsernameMessage";
|
||||||
addMessage = "invalidUsernameMessage";
|
case "invalidPassword" -> "invalidPasswordMessage";
|
||||||
break;
|
default -> null;
|
||||||
case "invalidPassword":
|
};
|
||||||
addMessage = "invalidPasswordMessage";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
model.addAttribute("addMessage", addMessage);
|
model.addAttribute("addMessage", addMessage);
|
||||||
}
|
}
|
||||||
String changeMessage = null;
|
|
||||||
|
String changeMessage;
|
||||||
if (messageType != null) {
|
if (messageType != null) {
|
||||||
switch (messageType) {
|
changeMessage =
|
||||||
case "userNotFound":
|
switch (messageType) {
|
||||||
changeMessage = "userNotFoundMessage";
|
case "userNotFound" -> "userNotFoundMessage";
|
||||||
break;
|
case "downgradeCurrentUser" -> "downgradeCurrentUserMessage";
|
||||||
case "downgradeCurrentUser":
|
case "disabledCurrentUser" -> "disabledCurrentUserMessage";
|
||||||
changeMessage = "downgradeCurrentUserMessage";
|
default -> messageType;
|
||||||
break;
|
};
|
||||||
case "disabledCurrentUser":
|
|
||||||
changeMessage = "disabledCurrentUserMessage";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
changeMessage = messageType;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
model.addAttribute("changeMessage", changeMessage);
|
model.addAttribute("changeMessage", changeMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("users", sortedUsers);
|
model.addAttribute("users", sortedUsers);
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
model.addAttribute("roleDetails", roleDetails);
|
model.addAttribute("roleDetails", roleDetails);
|
||||||
@@ -335,81 +327,54 @@ public class AccountWebController {
|
|||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication.isAuthenticated()) {
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = null;
|
String username = null;
|
||||||
if (principal instanceof UserDetails) {
|
|
||||||
// Cast the principal object to UserDetails
|
// Retrieve username and other attributes and add login attributes to the model
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
// Retrieve username and other attributes
|
username = detailsUser.getUsername();
|
||||||
username = userDetails.getUsername();
|
|
||||||
// Add oAuth2 Login attributes to the model
|
|
||||||
model.addAttribute("oAuth2Login", false);
|
model.addAttribute("oAuth2Login", false);
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User oAuth2User) {
|
||||||
// Cast the principal object to OAuth2User
|
username = oAuth2User.getName();
|
||||||
OAuth2User userDetails = (OAuth2User) principal;
|
|
||||||
// Retrieve username and other attributes
|
|
||||||
username =
|
|
||||||
userDetails.getAttribute(
|
|
||||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
|
||||||
// Add oAuth2 Login attributes to the model
|
|
||||||
model.addAttribute("oAuth2Login", true);
|
model.addAttribute("oAuth2Login", true);
|
||||||
}
|
}
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||||
// Cast the principal object to OAuth2User
|
username = saml2User.name();
|
||||||
CustomSaml2AuthenticatedPrincipal userDetails =
|
model.addAttribute("saml2Login", true);
|
||||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
|
||||||
// Retrieve username and other attributes
|
|
||||||
username = userDetails.getName();
|
|
||||||
// Add oAuth2 Login attributes to the model
|
|
||||||
model.addAttribute("oAuth2Login", true);
|
|
||||||
}
|
}
|
||||||
if (username != null) {
|
if (username != null) {
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user =
|
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||||
userRepository
|
|
||||||
.findByUsernameIgnoreCaseWithSettings( // Assuming findByUsername
|
if (user.isEmpty()) {
|
||||||
// method exists
|
|
||||||
username);
|
|
||||||
if (!user.isPresent()) {
|
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert settings map to JSON string
|
// Convert settings map to JSON string
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
String settingsJson;
|
String settingsJson;
|
||||||
try {
|
try {
|
||||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
// Handle JSON conversion error
|
log.error("Error converting settings map", e);
|
||||||
log.error("exception", e);
|
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
|
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
if (messageType != null) {
|
if (messageType != null) {
|
||||||
switch (messageType) {
|
switch (messageType) {
|
||||||
case "notAuthenticated":
|
case "notAuthenticated" -> messageType = "notAuthenticatedMessage";
|
||||||
messageType = "notAuthenticatedMessage";
|
case "userNotFound" -> messageType = "userNotFoundMessage";
|
||||||
break;
|
case "incorrectPassword" -> messageType = "incorrectPasswordMessage";
|
||||||
case "userNotFound":
|
case "usernameExists" -> messageType = "usernameExistsMessage";
|
||||||
messageType = "userNotFoundMessage";
|
case "invalidUsername" -> messageType = "invalidUsernameMessage";
|
||||||
break;
|
|
||||||
case "incorrectPassword":
|
|
||||||
messageType = "incorrectPasswordMessage";
|
|
||||||
break;
|
|
||||||
case "usernameExists":
|
|
||||||
messageType = "usernameExistsMessage";
|
|
||||||
break;
|
|
||||||
case "invalidUsername":
|
|
||||||
messageType = "invalidUsernameMessage";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
model.addAttribute("messageType", messageType);
|
|
||||||
}
|
}
|
||||||
// Add attributes to the model
|
|
||||||
model.addAttribute("username", username);
|
model.addAttribute("username", username);
|
||||||
|
model.addAttribute("messageType", messageType);
|
||||||
model.addAttribute("role", user.get().getRolesAsString());
|
model.addAttribute("role", user.get().getRolesAsString());
|
||||||
model.addAttribute("settings", settingsJson);
|
model.addAttribute("settings", settingsJson);
|
||||||
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
||||||
@@ -428,21 +393,14 @@ public class AccountWebController {
|
|||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return "redirect:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
if (authentication != null && authentication.isAuthenticated()) {
|
if (authentication.isAuthenticated()) {
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
if (principal instanceof UserDetails) {
|
if (principal instanceof UserDetails detailsUser) {
|
||||||
// Cast the principal object to UserDetails
|
String username = detailsUser.getUsername();
|
||||||
UserDetails userDetails = (UserDetails) principal;
|
|
||||||
// Retrieve username and other attributes
|
|
||||||
String username = userDetails.getUsername();
|
|
||||||
// Fetch user details from the database
|
// Fetch user details from the database
|
||||||
Optional<User> user =
|
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||||
userRepository
|
if (user.isEmpty()) {
|
||||||
.findByUsernameIgnoreCase( // Assuming findByUsername method exists
|
// Handle error appropriately, example redirection in case of error
|
||||||
username);
|
|
||||||
if (!user.isPresent()) {
|
|
||||||
// Handle error appropriately
|
|
||||||
// Example redirection in case of error
|
|
||||||
return "redirect:/error";
|
return "redirect:/error";
|
||||||
}
|
}
|
||||||
String messageType = request.getParameter("messageType");
|
String messageType = request.getParameter("messageType");
|
||||||
@@ -465,7 +423,7 @@ public class AccountWebController {
|
|||||||
}
|
}
|
||||||
model.addAttribute("messageType", messageType);
|
model.addAttribute("messageType", messageType);
|
||||||
}
|
}
|
||||||
// Add attributes to the model
|
|
||||||
model.addAttribute("username", username);
|
model.addAttribute("username", username);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user