Compare commits

...

20 Commits

Author SHA1 Message Date
Anthony Stirling
3c7b83ee87 Update survey version cache (#3071)
# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-27 10:18:26 +00:00
Ludy
9152e64b9f Remove convertBookTypeToPdf and Improve File Sanitization in FileToPdf (#3072)
# Description of Changes

Please provide a summary of the changes, including:

- **Removed `convertBookTypeToPdf` method**: 
- This method used `ebook-convert` from Calibre, which required external
dependencies.
- Its removal eliminates unnecessary process execution and simplifies
the codebase.
  
- **Enhanced `sanitizeZipFilename` function**:
  - Added handling for drive letters (e.g., `C:\`).
  - Ensured all slashes are normalized to forward slashes.
- Improved recursive path traversal removal to prevent directory escape
vulnerabilities.

- **Refactored `ProcessExecutor` output handling**:
  - Replaced redundant `.size() > 0` checks with `.isEmpty()`.
  
- **Expanded unit tests in `FileToPdfTest`**:
  - Added tests for `sanitizeZipFilename` to cover edge cases.
  - Improved test descriptions and added assertion messages.
  - Added debug print statements for easier test debugging.

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-26 19:25:35 +00:00
stirlingbot[bot]
96655f7cac Update 3rd Party Licenses (#3070)
Auto-generated by StirlingBot

Signed-off-by: stirlingbot[bot] <1113334+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-02-26 16:46:23 +00:00
dependabot[bot]
8f7153b30a Bump ch.qos.logback:logback-classic from 1.5.16 to 1.5.17 (#3069)
Bumps
[ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from
1.5.16 to 1.5.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-classic's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.17</h2>
<p><strong>2025-02-25 Release of logback version 1.5.17</strong></p>
<p>• Fixed Jansi 2.4.0 color-coded output not working on Windows CMD.exe
console when the default terminal application is set to &quot;Windows
Console Host&quot;. This problem was reported in issues/753 by Michael
Lyubkin.</p>
<p>• Fixed race condition occurring in case MDC class is initialized
while org.slf4j.LoggerFactory is initializing logback-classic's
LoggerContext. When this race conditions occurs, the MDCAdapter instance
used by MDC does not match the instance used by logback-classic. This
issue was reported in SLF4J issues/450. While logback-classic version
1.5.17 remains compatible with SLF4J versions in the 2.0.x series,
fixing this particular MDC issue requires SLF4J version 2.0.17.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
10358724ed723b3745c010aa40cb02a2dfed4593 associated with the tag
v_1.5.17. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="10358724ed"><code>1035872</code></a>
prepare release 1.5.17</li>
<li><a
href="2e6984d1e1"><code>2e6984d</code></a>
bump to slf4j version 2.0.17</li>
<li><a
href="100995244b"><code>1009952</code></a>
use a new LoggerContert instance when running LogbackListenerTest. This
shoul...</li>
<li><a
href="a3bb4b096a"><code>a3bb4b0</code></a>
Merge branch 'master' of github.com:qos-ch/logback</li>
<li><a
href="b507297eaa"><code>b507297</code></a>
Fixed race condition occurring in case MDC class is initialized while
org.slf...</li>
<li><a
href="f5b3bc56cd"><code>f5b3bc5</code></a>
add warning about the deprecation of SerializedModelConfigurator if
activated</li>
<li><a
href="5bc0998ce1"><code>5bc0998</code></a>
Update README.md</li>
<li><a
href="5610c96b4d"><code>5610c96</code></a>
correct relocation address</li>
<li><a
href="f3d100b89d"><code>f3d100b</code></a>
update logback-access evaluator examples</li>
<li><a
href="51e390303e"><code>51e3903</code></a>
fix issues/753 for the second time</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.16...v_1.5.17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ch.qos.logback:logback-classic&package-manager=gradle&previous-version=1.5.16&new-version=1.5.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 16:23:09 +00:00
dependabot[bot]
366bec602d Bump ch.qos.logback:logback-core from 1.5.16 to 1.5.17 (#3068)
Bumps [ch.qos.logback:logback-core](https://github.com/qos-ch/logback)
from 1.5.16 to 1.5.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/qos-ch/logback/releases">ch.qos.logback:logback-core's
releases</a>.</em></p>
<blockquote>
<h2>Logback 1.5.17</h2>
<p><strong>2025-02-25 Release of logback version 1.5.17</strong></p>
<p>• Fixed Jansi 2.4.0 color-coded output not working on Windows CMD.exe
console when the default terminal application is set to &quot;Windows
Console Host&quot;. This problem was reported in issues/753 by Michael
Lyubkin.</p>
<p>• Fixed race condition occurring in case MDC class is initialized
while org.slf4j.LoggerFactory is initializing logback-classic's
LoggerContext. When this race conditions occurs, the MDCAdapter instance
used by MDC does not match the instance used by logback-classic. This
issue was reported in SLF4J issues/450. While logback-classic version
1.5.17 remains compatible with SLF4J versions in the 2.0.x series,
fixing this particular MDC issue requires SLF4J version 2.0.17.</p>
<p>• A bit-wise identical binary of this version can be reproduced by
building from source code at commit
10358724ed723b3745c010aa40cb02a2dfed4593 associated with the tag
v_1.5.17. Release built using Java &quot;21&quot; 2023-10-17 LTS build
21.0.1.+12-LTS-29 under Linux Debian 11.6.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="10358724ed"><code>1035872</code></a>
prepare release 1.5.17</li>
<li><a
href="2e6984d1e1"><code>2e6984d</code></a>
bump to slf4j version 2.0.17</li>
<li><a
href="100995244b"><code>1009952</code></a>
use a new LoggerContert instance when running LogbackListenerTest. This
shoul...</li>
<li><a
href="a3bb4b096a"><code>a3bb4b0</code></a>
Merge branch 'master' of github.com:qos-ch/logback</li>
<li><a
href="b507297eaa"><code>b507297</code></a>
Fixed race condition occurring in case MDC class is initialized while
org.slf...</li>
<li><a
href="f5b3bc56cd"><code>f5b3bc5</code></a>
add warning about the deprecation of SerializedModelConfigurator if
activated</li>
<li><a
href="5bc0998ce1"><code>5bc0998</code></a>
Update README.md</li>
<li><a
href="5610c96b4d"><code>5610c96</code></a>
correct relocation address</li>
<li><a
href="f3d100b89d"><code>f3d100b</code></a>
update logback-access evaluator examples</li>
<li><a
href="51e390303e"><code>51e3903</code></a>
fix issues/753 for the second time</li>
<li>Additional commits viewable in <a
href="https://github.com/qos-ch/logback/compare/v_1.5.16...v_1.5.17">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ch.qos.logback:logback-core&package-manager=gradle&previous-version=1.5.16&new-version=1.5.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-26 15:56:35 +00:00
Ludy
c9c8378fe0 Improve Case-Insensitive Key Comparison and Path Normalization in Language Properties Check Script (#3067)
# Description of Changes

Please provide a summary of the changes, including:

- Updated key comparison logic in `update_missing_keys` function to be
case-insensitive by converting keys to lowercase before comparison.
- Introduced `os.path.normpath` for file path normalization to improve
cross-platform compatibility.
- Replaced direct usage of `file_path` with `file_normpath` in security
checks, file size validation, and duplicate key detection to ensure
consistent path handling.

These changes improve the robustness and maintainability of the script,
ensuring more accurate language property checks while enhancing security
validation.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-26 15:56:03 +00:00
Dario Ghunney Ware
7a7338c6de OAuth 2 redirectUri hotfix (#3066)
# Description of Changes

- Reverted path in `OAuth2Configuration` for `redirectUri` back to
'oidc' to fix the Redirect Uri error users were facing when using SSO
with Authentik
- Changed log level for some logs

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [x] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-26 10:22:25 +00:00
Anthony Stirling
77dec10f25 Remove book (#3065)
# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-26 00:46:11 +00:00
stirlingbot[bot]
12b03be2be 🌐 Sync Translations + Update README Progress Table (#3063)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files (`messages_*.properties`) to reflect changes
in the reference file `messages_en_GB.properties`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.

#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

---------

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-02-25 22:09:30 +00:00
stirlingbot[bot]
222c18cdae 🌐 Sync Translations + Update README Progress Table (#3050)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files (`messages_*.properties`) to reflect changes
in the reference file `messages_en_GB.properties`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.

#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.

#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.

---

Auto-generated by [create-pull-request][1].

[1]: https://github.com/peter-evans/create-pull-request

---------

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2025-02-25 22:02:12 +00:00
stirlingbot[bot]
d2bc281e42 Update 3rd Party Licenses (#3062)
Auto-generated by StirlingBot

Signed-off-by: stirlingbot[bot] <1113334+stirlingbot[bot]@users.noreply.github.com>
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2025-02-25 22:01:51 +00:00
Ludy
ac10c9fa43 Improved Configuration and YAML Management (#2966)
# Description of Changes

**What was changed:**
- **Configuration Updates:**  
Replaced all calls to `GeneralUtils.saveKeyToConfig` with the new
`GeneralUtils.saveKeyToSettings` method across multiple classes (e.g.,
`LicenseKeyChecker`, `InitialSetup`, `SettingsController`, etc.). This
update ensures consistent management of configuration settings.
  
- **File Path and Exception Handling:**  
Updated file path handling in `SPDFApplication` by creating `Path`
objects from string paths and logging these paths for clarity. Also
refined exception handling by catching more specific exceptions (e.g.,
using `IOException` instead of a generic `Exception`).

- **Analytics Flag and Rate Limiting:**  
Changed the analytics flag in the application properties from a `String`
to a `Boolean`, and updated related logic in `AppConfig` and
`PostHogService`. The rate-limiting property retrieval in `AppConfig`
was also refined for clarity.

- **YAML Configuration Management:**  
Replaced the previous manual, line-based YAML merging logic in
`ConfigInitializer` with a new `YamlHelper` class. This helper leverages
the SnakeYAML engine to load, update, and save YAML configurations more
robustly while preserving comments and formatting.

**Why the change was made:**
- **Improved Maintainability:**  
Consolidating configuration update logic into a single utility method
(`saveKeyToSettings`) reduces code duplication and simplifies future
maintenance.
  
- **Enhanced Robustness:**  
The new `YamlHelper` class ensures that configuration files are merged
accurately and safely, minimizing risks of data loss or format
corruption.
  
- **Better Type Safety and Exception Handling:**  
Switching the analytics flag to a Boolean and refining exception
handling improves code robustness and debugging efficiency.
  
- **Clarity and Consistency:**  
Standardizing file path handling and logging practices enhances code
readability across the project.

**Challenges encountered:**
- **YAML Merging Complexity:**  
Integrating the new `YamlHelper` required careful handling to preserve
existing settings, comments, and formatting during merges.
  
- **Type Conversion and Backward Compatibility:**  
Updating the analytics flag from a string to a Boolean required
extensive testing to ensure backward compatibility and proper
functionality.
  
- **Exception Granularity:**  
Refactoring exception handling from a generic to a more specific
approach involved a detailed review to cover all edge cases.

Closes #<issue_number>

---

## Checklist

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2025-02-25 21:52:59 +00:00
Anthony Stirling
4fabc07a44 add view pdf to nav and remove duplicate home on view (#3052)
# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-25 21:45:50 +00:00
Ludy
2ab951e080 Improve Type-Safe Casting with Pattern Matching (#2990)
# Description of Changes

Please provide a summary of the changes, including:

This PR refactors multiple instances of type casting throughout the
codebase by replacing them with Java's pattern matching for
`instanceof`. This approach eliminates redundant type casting, improves
code readability, and reduces the chances of `ClassCastException`. The
changes primarily affect authentication handling, PDF processing, and
certificate validation.

### Key Changes:
- Replaced traditional `instanceof` checks followed by explicit casting
with pattern matching.
- Improved readability and maintainability of type-related operations.
- Applied changes across security modules, PDF utilities, and image
processing functions.

This refactor does not introduce new functionality but enhances the
robustness and clarity of the existing code.

pending until #2818 is published

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-25 21:31:50 +00:00
Ludy
a1f7bb3e4a Refactor Path Handling (#3041)
# Description of Changes

Please provide a summary of the changes, including:

What was changed:

- Refactored path constructions in multiple classes (e.g.,
SPDFApplication.java, InstallationPathConfig.java,
RuntimePathConfig.java) to use Java NIO’s Paths.get() and Path.of()
instead of manual string concatenation.


Why the change was made:

- To improve code readability, maintainability, and robustness by
leveraging modern Java NIO utilities.
- To ensure better portability across different operating systems by
avoiding hardcoded file separators.


Challenges encountered:

- Maintaining backward compatibility while transitioning from manual
string concatenation to using Paths for file path construction.
- Ensuring that the refactored path resolution works consistently across
all supported environments (Windows, macOS, Linux, and Docker).

@Frooodle can you check the docker path `/.dockerenv`?

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-25 21:31:20 +00:00
dependabot[bot]
f64d7d42d9 Bump peter-evans/create-pull-request from 7.0.6 to 7.0.7 (#3051)
Bumps
[peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request)
from 7.0.6 to 7.0.7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's
releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v7.0.7</h2>
<p>⚙️ Fixes an issue with commit signing where modifications to the same
file in multiple commits squash into the first commit.</p>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump <code>@​octokit/core</code> from 6.1.2 to 6.1.3 by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3593">peter-evans/create-pull-request#3593</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.68 to
18.19.70 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3594">peter-evans/create-pull-request#3594</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3603">peter-evans/create-pull-request#3603</a></li>
<li>build(deps-dev): bump typescript from 5.7.2 to 5.7.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3610">peter-evans/create-pull-request#3610</a></li>
<li>build(deps): bump octokit dependencies by <a
href="https://github.com/peter-evans"><code>@​peter-evans</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3618">peter-evans/create-pull-request#3618</a></li>
<li>docs: add workflow tip for showing message via workflow command by
<a href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3626">peter-evans/create-pull-request#3626</a></li>
<li>build(deps-dev): bump eslint-plugin-prettier from 5.2.1 to 5.2.3 by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3628">peter-evans/create-pull-request#3628</a></li>
<li>build(deps): bump node-fetch-native from 1.6.4 to 1.6.6 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3627">peter-evans/create-pull-request#3627</a></li>
<li>build(deps-dev): bump undici from 6.21.0 to 6.21.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3630">peter-evans/create-pull-request#3630</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.70 to
18.19.71 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3629">peter-evans/create-pull-request#3629</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3647">peter-evans/create-pull-request#3647</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.71 to
18.19.74 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3657">peter-evans/create-pull-request#3657</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.74 to
18.19.75 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3663">peter-evans/create-pull-request#3663</a></li>
<li>build(deps): bump
<code>@​octokit/plugin-rest-endpoint-methods</code> from 13.3.0 to
13.3.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3670">peter-evans/create-pull-request#3670</a></li>
<li>build(deps-dev): bump prettier from 3.4.2 to 3.5.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3671">peter-evans/create-pull-request#3671</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3680">peter-evans/create-pull-request#3680</a></li>
<li>build(deps): bump <code>@​octokit/request-error</code> from 6.1.6 to
6.1.7 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3685">peter-evans/create-pull-request#3685</a></li>
<li>build(deps): bump <code>@​octokit/plugin-paginate-rest</code> from
11.4.0 to 11.4.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3688">peter-evans/create-pull-request#3688</a></li>
<li>build(deps): bump <code>@​octokit/endpoint</code> from 10.1.2 to
10.1.3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3700">peter-evans/create-pull-request#3700</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3691">peter-evans/create-pull-request#3691</a></li>
<li>build(deps-dev): bump prettier from 3.5.0 to 3.5.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3709">peter-evans/create-pull-request#3709</a></li>
<li>build(deps-dev): bump eslint-import-resolver-typescript from 3.7.0
to 3.8.1 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3710">peter-evans/create-pull-request#3710</a></li>
<li>build(deps): bump <code>@​octokit/plugin-paginate-rest</code> from
11.4.1 to 11.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3713">peter-evans/create-pull-request#3713</a></li>
<li>build(deps-dev): bump <code>@​types/node</code> from 18.19.75 to
18.19.76 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3712">peter-evans/create-pull-request#3712</a></li>
<li>build(deps): bump <code>@​octokit/core</code> from 6.1.3 to 6.1.4 by
<a href="https://github.com/dependabot"><code>@​dependabot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3711">peter-evans/create-pull-request#3711</a></li>
<li>Update distribution by <a
href="https://github.com/actions-bot"><code>@​actions-bot</code></a> in
<a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3736">peter-evans/create-pull-request#3736</a></li>
<li>Use showFileAtRefBase64 to read per-commit file contents by <a
href="https://github.com/grahamc"><code>@​grahamc</code></a> in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3744">peter-evans/create-pull-request#3744</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/ybiquitous"><code>@​ybiquitous</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3626">peter-evans/create-pull-request#3626</a></li>
<li><a href="https://github.com/grahamc"><code>@​grahamc</code></a> made
their first contribution in <a
href="https://redirect.github.com/peter-evans/create-pull-request/pull/3744">peter-evans/create-pull-request#3744</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/peter-evans/create-pull-request/compare/v7.0.6...v7.0.7">https://github.com/peter-evans/create-pull-request/compare/v7.0.6...v7.0.7</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dd2324fc52"><code>dd2324f</code></a>
fix: use showFileAtRefBase64 to read per-commit file contents (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3744">#3744</a>)</li>
<li><a
href="367180cbdf"><code>367180c</code></a>
ci: remove testv5 cmd</li>
<li><a
href="25575a12f3"><code>25575a1</code></a>
build: update distribution (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3736">#3736</a>)</li>
<li><a
href="a56e7a56e9"><code>a56e7a5</code></a>
build(deps): bump <code>@​octokit/core</code> from 6.1.3 to 6.1.4 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3711">#3711</a>)</li>
<li><a
href="eac17dc6a3"><code>eac17dc</code></a>
build(deps-dev): bump <code>@​types/node</code> from 18.19.75 to
18.19.76 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3712">#3712</a>)</li>
<li><a
href="a2e685f814"><code>a2e685f</code></a>
build(deps): bump <code>@​octokit/plugin-paginate-rest</code> from
11.4.1 to 11.4.2 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3713">#3713</a>)</li>
<li><a
href="6cfd146ec9"><code>6cfd146</code></a>
build(deps-dev): bump eslint-import-resolver-typescript (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3710">#3710</a>)</li>
<li><a
href="b38e8d38a1"><code>b38e8d3</code></a>
build(deps-dev): bump prettier from 3.5.0 to 3.5.1 (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3709">#3709</a>)</li>
<li><a
href="8a41570d99"><code>8a41570</code></a>
build: update distribution (<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3691">#3691</a>)</li>
<li><a
href="2e9b4cc10e"><code>2e9b4cc</code></a>
build(deps): bump <code>@​octokit/endpoint</code> from 10.1.2 to 10.1.3
(<a
href="https://redirect.github.com/peter-evans/create-pull-request/issues/3700">#3700</a>)</li>
<li>Additional commits viewable in <a
href="67ccf781d6...dd2324fc52">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=peter-evans/create-pull-request&package-manager=github_actions&previous-version=7.0.6&new-version=7.0.7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-25 21:29:06 +00:00
albanobattistella
300011f9b6 Update messages_it_IT.properties (#3055)
# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-25 21:27:59 +00:00
Ludy
e328833f02 Restrict Backup Import to Initialization Process and Refactor API Key Handling (#3061)
# Description of Changes

Please provide a summary of the changes, including:

- **What was changed:**
- Updated the backup import logic in `InitialSecuritySetup` so that the
database backup is only imported during initialization when there are no
users present. If no backup exists, the admin user is initialized
instead.
- Refactored the API key addition in `UserService` by extracting the
logic into a private helper method `saveUser(Optional<User> user)` and
added a call to export the database after updating the user's API key.

- **Why the change was made:**
- To prevent accidental or unintended backup imports outside the
initialization process, ensuring the system only imports backups when
necessary.
- To improve code clarity and maintainability in the user API key
management process, while ensuring that the database state is preserved
via an export after key updates.

Closes https://github.com/Stirling-Tools/Stirling-PDF/discussions/3057

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-25 21:24:01 +00:00
Dario Ghunney Ware
4c701b2e69 SSO Refactoring (#2818)
# Description of Changes

* Refactoring of SSO code around OAuth & SAML 2
* Enabling auto-login with SAML 2 via the new `SSOAutoLogin` property
* Correcting typos & general cleanup

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [x] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [x] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [x] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-24 22:18:34 +00:00
Ludy
16295c7bb9 move compress.grayscale.label to section #compress (#3048)
# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
2025-02-24 22:13:20 +00:00
121 changed files with 2379 additions and 1879 deletions

View File

@@ -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)
) )
] ]
) )

View File

@@ -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"

View File

@@ -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"

View File

@@ -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

View File

@@ -25,7 +25,7 @@ ext {
} }
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.16' implementation 'ch.qos.logback:logback-core:1.5.17'
implementation 'ch.qos.logback:logback-classic:1.5.16' implementation 'ch.qos.logback:logback-classic:1.5.17'
// Exclude vulnerable BouncyCastle version used in tableau // Exclude vulnerable BouncyCastle version used in tableau
@@ -347,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"

View File

@@ -20,6 +20,7 @@ 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();
@@ -68,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;
} }
} }
@@ -95,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();
@@ -120,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;
} }

View File

@@ -51,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();
} }

View File

@@ -83,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()) {
@@ -102,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();
@@ -128,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());
} }
@@ -157,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());
} }
} }

View File

@@ -35,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));
@@ -99,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")
@@ -129,8 +126,8 @@ public class AppConfig {
} }
@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;
} }
@@ -173,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")

View File

@@ -20,7 +20,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
"endpoints", "endpoints",
"logout", "logout",
"error", "error",
"erroroauth", "errorOAuth",
"file", "file",
"messageType", "messageType",
"infoMessage"); "infoMessage");

View File

@@ -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<>();
}
} }

View File

@@ -44,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);
} }
} }
@@ -54,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);
} }
} }
@@ -64,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);
} }
} }
@@ -76,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);
} }
} }
@@ -97,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);
} }
} }

View File

@@ -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;
@@ -46,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() {

View File

@@ -1,8 +1,7 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.File;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -33,52 +32,48 @@ public class RuntimePathConfig {
this.properties = properties; this.properties = properties;
this.basePath = InstallationPathConfig.getPath(); this.basePath = InstallationPathConfig.getPath();
String pipelinePath = basePath + "pipeline" + File.separator; this.pipelinePath = Path.of(basePath, "pipeline").toString();
String watchedFoldersPath = pipelinePath + "watchedFolders" + File.separator; String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString();
String finishedFoldersPath = pipelinePath + "finishedFolders" + File.separator; String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
String webUiConfigsPath = pipelinePath + "defaultWebUIConfigs" + File.separator; String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline(); Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
if (pipeline != null) {
if (!StringUtils.isEmpty(pipeline.getWatchedFoldersDir())) {
watchedFoldersPath = pipeline.getWatchedFoldersDir();
}
if (!StringUtils.isEmpty(pipeline.getFinishedFoldersDir())) {
finishedFoldersPath = pipeline.getFinishedFoldersDir();
}
if (!StringUtils.isEmpty(pipeline.getWebUIConfigsDir())) {
webUiConfigsPath = pipeline.getWebUIConfigsDir();
}
}
this.pipelinePath = pipelinePath; this.pipelineWatchedFoldersPath =
this.pipelineWatchedFoldersPath = watchedFoldersPath; resolvePath(
this.pipelineFinishedFoldersPath = finishedFoldersPath; defaultWatchedFolders,
this.pipelineDefaultWebUiConfigs = webUiConfigsPath; 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(); boolean isDocker = isRunningInDocker();
// Initialize Operation paths // Initialize Operation paths
String weasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint"; String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
String unoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert"; String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
// Check for custom operation paths
Operations operations = properties.getSystem().getCustomPaths().getOperations(); Operations operations = properties.getSystem().getCustomPaths().getOperations();
if (operations != null) { this.weasyPrintPath =
if (!StringUtils.isEmpty(operations.getWeasyprint())) { resolvePath(
weasyPrintPath = operations.getWeasyprint(); defaultWeasyPrintPath,
} operations != null ? operations.getWeasyprint() : null);
if (!StringUtils.isEmpty(operations.getUnoconvert())) { this.unoConvertPath =
unoConvertPath = operations.getUnoconvert(); resolvePath(
} defaultUnoConvertPath,
} operations != null ? operations.getUnoconvert() : null);
}
// Assign operations final fields private String resolvePath(String defaultPath, String customPath) {
this.weasyPrintPath = weasyPrintPath; return StringUtils.isNotBlank(customPath) ? customPath : defaultPath;
this.unoConvertPath = unoConvertPath;
} }
private boolean isRunningInDocker() { private boolean isRunningInDocker() {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Path.of("/.dockerenv"));
} }
} }

View 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());
}
}
}

View File

@@ -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 {

View File

@@ -69,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

View File

@@ -14,8 +14,8 @@ 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;
@@ -28,79 +28,61 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
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
@@ -111,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();
@@ -143,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 ]", "");
} }

View File

@@ -25,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

View File

@@ -12,7 +12,7 @@ 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
@@ -36,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();

View File

@@ -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;
@@ -51,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;
@@ -109,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);
@@ -164,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
@@ -227,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(
@@ -258,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();
@@ -284,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();
@@ -315,7 +311,7 @@ public class SecurityConfiguration {
} }
@Bean @Bean
public boolean activSecurity() { public boolean activeSecurity() {
return true; return true;
} }
} }

View File

@@ -88,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;
@@ -123,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;
} }
} }
@@ -141,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;
} }
@@ -170,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;
} }
@@ -193,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) {

View File

@@ -27,7 +27,7 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
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;
@@ -78,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) {
@@ -123,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) {
@@ -173,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)) {
@@ -373,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());
@@ -398,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();
}
} }
} }

View File

@@ -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;
@@ -14,7 +12,7 @@ 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
@@ -37,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;
} }

View File

@@ -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)

View File

@@ -29,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) {
@@ -42,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);

View File

@@ -20,19 +20,18 @@ 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;
@@ -48,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
@@ -78,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");
@@ -87,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) {

View File

@@ -17,19 +17,19 @@ 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,
@@ -42,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");
} }
} }
@@ -79,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);

View File

@@ -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;
@@ -26,18 +29,20 @@ 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;
@@ -48,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<>();
@@ -189,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()
@@ -197,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()));
}
} }
} }
}); });

View File

@@ -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: "

View File

@@ -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;
}
} }

View File

@@ -2,19 +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
@@ -22,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);
} }
} }

View File

@@ -21,7 +21,7 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.model.exception.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@@ -41,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);
@@ -97,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;
} }
@@ -105,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 {

View File

@@ -7,6 +7,7 @@ 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;
@@ -18,10 +19,11 @@ 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;
@@ -61,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")) {
@@ -84,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<>();
@@ -102,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) {

View File

@@ -11,10 +11,12 @@ 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;
@@ -26,27 +28,20 @@ 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();
@@ -58,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"));
}
} }

View File

@@ -43,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) {
@@ -74,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) {

View File

@@ -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");
} }
} }

View File

@@ -36,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")
@@ -126,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();
@@ -154,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();
@@ -176,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
@@ -199,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);
} }
} }
@@ -276,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)) {
@@ -295,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());
} }
} }
} }

View File

@@ -61,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,
@@ -73,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);
@@ -91,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));
@@ -108,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();

View File

@@ -63,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);
@@ -119,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();
@@ -221,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;
} }

View File

@@ -118,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 {
@@ -139,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;
@@ -180,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 {
@@ -191,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

View File

@@ -129,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();
@@ -201,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();
} }

View File

@@ -214,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());
@@ -437,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());
@@ -462,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
} }
@@ -541,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();
@@ -698,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));
@@ -724,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

View File

@@ -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);
} }
} }
} }

View File

@@ -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;
@@ -24,12 +31,16 @@ 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;
@@ -39,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,
@@ -61,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";
} }
@@ -230,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);
} }
@@ -273,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);
@@ -337,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());
@@ -430,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");
@@ -467,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 {

View File

@@ -230,7 +230,11 @@ public class GeneralWebController {
// Extract font names from external directory // Extract font names from external directory
fontNames.addAll( fontNames.addAll(
getFontNamesFromLocation( getFontNamesFromLocation(
"file:" + InstallationPathConfig.getStaticPath() + "fonts/*")); "file:"
+ InstallationPathConfig.getStaticPath()
+ "fonts"
+ File.separator
+ "*"));
return fontNames; return fontNames;
} }

View File

@@ -1,5 +1,7 @@
package stirling.software.SPDF.model; package stirling.software.SPDF.model;
import static stirling.software.SPDF.utils.validation.Validator.*;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
@@ -12,7 +14,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -34,10 +35,11 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.InstallationPathConfig; import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.config.YamlPropertySourceFactory; import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider; import stirling.software.SPDF.model.exception.UnsupportedProviderException;
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.UnsupportedProviderException; import stirling.software.SPDF.model.provider.Provider;
@Configuration @Configuration
@ConfigurationProperties(prefix = "") @ConfigurationProperties(prefix = "")
@@ -136,13 +138,13 @@ public class ApplicationProperties {
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
} }
public boolean isOauth2Activ() { public boolean isOauth2Active() {
return (oauth2 != null return (oauth2 != null
&& oauth2.getEnabled() && oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
} }
public boolean isSaml2Activ() { public boolean isSaml2Active() {
return (saml2 != null return (saml2 != null
&& saml2.getEnabled() && saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
@@ -158,6 +160,7 @@ public class ApplicationProperties {
@Setter @Setter
@ToString @ToString
public static class SAML2 { public static class SAML2 {
private String provider;
private Boolean enabled = false; private Boolean enabled = false;
private Boolean autoCreateUser = false; private Boolean autoCreateUser = false;
private Boolean blockRegistration = false; private Boolean blockRegistration = false;
@@ -195,7 +198,7 @@ public class ApplicationProperties {
} }
} }
public Resource getidpCert() { public Resource getIdpCert() {
if (idpCert == null) return null; if (idpCert == null) return null;
if (idpCert.startsWith("classpath:")) { if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length())); return new ClassPathResource(idpCert.substring("classpath:".length()));
@@ -225,12 +228,11 @@ public class ApplicationProperties {
private Collection<String> scopes = new ArrayList<>(); private Collection<String> scopes = new ArrayList<>();
private String provider; private String provider;
private Client client = new Client(); private Client client = new Client();
private String logoutUrl;
public void setScopes(String scopes) { public void setScopes(String scopes) {
List<String> scopesList = List<String> scopesList =
Arrays.stream(scopes.split(",")) Arrays.stream(scopes.split(",")).map(String::trim).toList();
.map(String::trim)
.collect(Collectors.toList());
this.scopes.addAll(scopesList); this.scopes.addAll(scopesList);
} }
@@ -243,32 +245,31 @@ public class ApplicationProperties {
} }
public boolean isSettingsValid() { public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer") return !isStringEmpty(this.getIssuer())
&& isValid(this.getClientId(), "clientId") && !isStringEmpty(this.getClientId())
&& isValid(this.getClientSecret(), "clientSecret") && !isStringEmpty(this.getClientSecret())
&& isValid(this.getScopes(), "scopes") && !isCollectionEmpty(this.getScopes())
&& isValid(this.getUseAsUsername(), "useAsUsername"); && !isStringEmpty(this.getUseAsUsername());
} }
@Data @Data
public static class Client { public static class Client {
private GoogleProvider google = new GoogleProvider(); private GoogleProvider google = new GoogleProvider();
private GithubProvider github = new GithubProvider(); private GitHubProvider github = new GitHubProvider();
private KeycloakProvider keycloak = new KeycloakProvider(); private KeycloakProvider keycloak = new KeycloakProvider();
public Provider get(String registrationId) throws UnsupportedProviderException { public Provider get(String registrationId) throws UnsupportedProviderException {
switch (registrationId.toLowerCase()) { return switch (registrationId.toLowerCase()) {
case "google": case "google" -> getGoogle();
return getGoogle(); case "github" -> getGithub();
case "github": case "keycloak" -> getKeycloak();
return getGithub(); default ->
case "keycloak": throw new UnsupportedProviderException(
return getKeycloak(); "Logout from the provider "
default: + registrationId
throw new UnsupportedProviderException( + " is not supported. "
"Logout from the provider is not supported? Report it at" + "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues"); };
}
} }
} }
} }
@@ -283,10 +284,14 @@ public class ApplicationProperties {
private boolean customHTMLFiles; private boolean customHTMLFiles;
private String tessdataDir; private String tessdataDir;
private Boolean enableAlphaFunctionality; private Boolean enableAlphaFunctionality;
private String enableAnalytics; private Boolean enableAnalytics;
private Datasource datasource; private Datasource datasource;
private Boolean disableSanitize; private Boolean disableSanitize;
private CustomPaths customPaths = new CustomPaths(); private CustomPaths customPaths = new CustomPaths();
public boolean isAnalyticsEnabled() {
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
}
} }
@Data @Data
@@ -335,10 +340,10 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return """ return """
Driver { Driver {
driverName='%s' driverName='%s'
} }
""" """
.formatted(driverName); .formatted(driverName);
} }
} }

View File

@@ -1,80 +0,0 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public class Provider implements ProviderInterface {
private String name;
private String clientName;
public String getName() {
return name;
}
public String getClientName() {
return clientName;
}
protected boolean isValid(String value, String name) {
if (value != null && !value.trim().isEmpty()) {
return true;
}
return false;
}
protected boolean isValid(Collection<String> value, String name) {
if (value != null && !value.isEmpty()) {
return true;
}
return false;
}
@Override
public Collection<String> getScopes() {
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
}
@Override
public void setScopes(String scopes) {
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
}
@Override
public String getUseAsUsername() {
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
}
@Override
public void setUseAsUsername(String useAsUsername) {
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
}
@Override
public String getIssuer() {
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
}
@Override
public void setIssuer(String issuer) {
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
}
@Override
public String getClientSecret() {
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
}
@Override
public void setClientSecret(String clientSecret) {
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
}
@Override
public String getClientId() {
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
}
@Override
public void setClientId(String clientId) {
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
}
}

View File

@@ -1,26 +0,0 @@
package stirling.software.SPDF.model;
import java.util.Collection;
public interface ProviderInterface {
public Collection<String> getScopes();
public void setScopes(String scopes);
public String getUseAsUsername();
public void setUseAsUsername(String useAsUsername);
public String getIssuer();
public void setIssuer(String issuer);
public String getClientSecret();
public void setClientSecret(String clientSecret);
public String getClientId();
public void setClientId(String clientId);
}

View File

@@ -0,0 +1,24 @@
package stirling.software.SPDF.model;
import lombok.Getter;
@Getter
public enum UsernameAttribute {
EMAIL("email"),
LOGIN("login"),
PROFILE("profile"),
NAME("name"),
USERNAME("username"),
NICKNAME("nickname"),
GIVEN_NAME("given_name"),
MIDDLE_NAME("middle_name"),
FAMILY_NAME("family_name"),
PREFERRED_NAME("preferred_name"),
PREFERRED_USERNAME("preferred_username");
private final String name;
UsernameAttribute(final String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,11 @@
package stirling.software.SPDF.model.exception;
public class NoProviderFoundException extends Exception {
public NoProviderFoundException(String message) {
super(message);
}
public NoProviderFoundException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,4 +1,4 @@
package stirling.software.SPDF.model.provider; package stirling.software.SPDF.model.exception;
public class UnsupportedProviderException extends Exception { public class UnsupportedProviderException extends Exception {
public UnsupportedProviderException(String message) { public UnsupportedProviderException(String message) {

View File

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

View File

@@ -0,0 +1,87 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Collection;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class GitHubProvider extends Provider {
private static final String NAME = "github";
private static final String CLIENT_NAME = "GitHub";
private static final String AUTHORIZATION_URI = "https://github.com/login/oauth/authorize";
private static final String TOKEN_URI = "https://github.com/login/oauth/access_token";
private static final String USER_INFO_URI = "https://api.github.com/user";
public GitHubProvider(
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
null,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN,
null,
AUTHORIZATION_URI,
TOKEN_URI,
USER_INFO_URI);
}
@Override
public String getAuthorizationUri() {
return AUTHORIZATION_URI;
}
@Override
public String getTokenUri() {
return TOKEN_URI;
}
@Override
public String getUserInfoUri() {
return USER_INFO_URI;
}
@Override
public String getName() {
return NAME;
}
@Override
public String getClientName() {
return CLIENT_NAME;
}
@Override
public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("read:user");
}
return scopes;
}
@Override
public String toString() {
return "GitHub [clientId="
+ getClientId()
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ "]";
}
}

View File

@@ -1,114 +0,0 @@
package stirling.software.SPDF.model.provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider;
public class GithubProvider extends Provider {
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
private static final String tokenUri = "https://github.com/login/oauth/access_token";
private static final String userInfoUri = "https://api.github.com/user";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
public String getAuthorizationuri() {
return authorizationUri;
}
public String getTokenuri() {
return tokenUri;
}
public String getUserinfouri() {
return userInfoUri;
}
@Override
public String getIssuer() {
return new String();
}
@Override
public void setIssuer(String issuer) {}
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
@Override
public Collection<String> getScopes() {
if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>();
scopes.add("read:user");
}
return scopes;
}
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override
public String toString() {
return "GitHub [clientId="
+ clientId
+ ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
+ ", scopes="
+ scopes
+ ", useAsUsername="
+ useAsUsername
+ "]";
}
@Override
public String getName() {
return "github";
}
@Override
public String getClientName() {
return "GitHub";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
}

View File

@@ -1,116 +1,86 @@
package stirling.software.SPDF.model.provider; package stirling.software.SPDF.model.provider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider; import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class GoogleProvider extends Provider { public class GoogleProvider extends Provider {
private static final String authorizationUri = "https://accounts.google.com/o/oauth2/v2/auth"; private static final String NAME = "google";
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token"; private static final String CLIENT_NAME = "Google";
private static final String userInfoUri = private static final String AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/v2/auth";
private static final String TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
private static final String USER_INFO_URI =
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json"; "https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
public String getAuthorizationuri() { public GoogleProvider(
return authorizationUri; String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
null,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername,
null,
AUTHORIZATION_URI,
TOKEN_URI,
USER_INFO_URI);
} }
public String getTokenuri() { public String getAuthorizationUri() {
return tokenUri; return AUTHORIZATION_URI;
} }
public String getUserinfouri() { public String getTokenUri() {
return userInfoUri; return TOKEN_URI;
}
public String getUserinfoUri() {
return USER_INFO_URI;
} }
@Override @Override
public String getIssuer() { public String getName() {
return new String(); return NAME;
} }
@Override @Override
public void setIssuer(String issuer) {} public String getClientName() {
return CLIENT_NAME;
@Override
public String getClientId() {
return this.clientId;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
} }
@Override @Override
public Collection<String> getScopes() { public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) { if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>(); scopes = new ArrayList<>();
scopes.add("https://www.googleapis.com/auth/userinfo.email"); scopes.add("https://www.googleapis.com/auth/userinfo.email");
scopes.add("https://www.googleapis.com/auth/userinfo.profile"); scopes.add("https://www.googleapis.com/auth/userinfo.profile");
} }
return scopes; return scopes;
} }
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override @Override
public String toString() { public String toString() {
return "Google [clientId=" return "Google [clientId="
+ clientId + getClientId()
+ ", clientSecret=" + ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL") + (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes=" + ", scopes="
+ scopes + getScopes()
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + getUseAsUsername()
+ "]"; + "]";
} }
@Override
public String getName() {
return "google";
}
@Override
public String getClientName() {
return "Google";
}
public boolean isSettingsValid() {
return super.isValid(this.getClientId(), "clientId")
&& super.isValid(this.getClientSecret(), "clientSecret")
&& super.isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
} }

View File

@@ -1,106 +1,73 @@
package stirling.software.SPDF.model.provider; package stirling.software.SPDF.model.provider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.stream.Collectors;
import stirling.software.SPDF.model.Provider; import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
@NoArgsConstructor
public class KeycloakProvider extends Provider { public class KeycloakProvider extends Provider {
private String issuer; private static final String NAME = "keycloak";
private String clientId; private static final String CLIENT_NAME = "Keycloak";
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
@Override public KeycloakProvider(
public String getIssuer() { String issuer,
return this.issuer; String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername) {
super(
issuer,
NAME,
CLIENT_NAME,
clientId,
clientSecret,
scopes,
useAsUsername,
null,
null,
null,
null);
} }
@Override @Override
public void setIssuer(String issuer) { public String getName() {
this.issuer = issuer; return NAME;
} }
@Override @Override
public String getClientId() { public String getClientName() {
return this.clientId; return CLIENT_NAME;
}
@Override
public void setClientId(String clientId) {
this.clientId = clientId;
}
@Override
public String getClientSecret() {
return this.clientSecret;
}
@Override
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
} }
@Override @Override
public Collection<String> getScopes() { public Collection<String> getScopes() {
Collection<String> scopes = super.getScopes();
if (scopes == null || scopes.isEmpty()) { if (scopes == null || scopes.isEmpty()) {
scopes = new ArrayList<>(); scopes = new ArrayList<>();
scopes.add("profile"); scopes.add("profile");
scopes.add("email"); scopes.add("email");
} }
return scopes; return scopes;
} }
@Override
public void setScopes(String scopes) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
@Override
public String getUseAsUsername() {
return this.useAsUsername;
}
@Override
public void setUseAsUsername(String useAsUsername) {
this.useAsUsername = useAsUsername;
}
@Override @Override
public String toString() { public String toString() {
return "Keycloak [issuer=" return "Keycloak [issuer="
+ issuer + getIssuer()
+ ", clientId=" + ", clientId="
+ clientId + getClientId()
+ ", clientSecret=" + ", clientSecret="
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL") + (getClientSecret() != null && !getClientSecret().isBlank() ? "*****" : "NULL")
+ ", scopes=" + ", scopes="
+ scopes + getScopes()
+ ", useAsUsername=" + ", useAsUsername="
+ useAsUsername + getUseAsUsername()
+ "]"; + "]";
} }
@Override
public String getName() {
return "keycloak";
}
@Override
public String getClientName() {
return "Keycloak";
}
public boolean isSettingsValid() {
return isValid(this.getIssuer(), "issuer")
&& isValid(this.getClientId(), "clientId")
&& isValid(this.getClientSecret(), "clientSecret")
&& isValid(this.getScopes(), "scopes")
&& isValid(this.getUseAsUsername(), "useAsUsername");
}
} }

View File

@@ -0,0 +1,134 @@
package stirling.software.SPDF.model.provider;
import static stirling.software.SPDF.model.UsernameAttribute.EMAIL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import lombok.Data;
import lombok.NoArgsConstructor;
import stirling.software.SPDF.model.UsernameAttribute;
import stirling.software.SPDF.model.exception.UnsupportedUsernameAttribute;
@Data
@NoArgsConstructor
public class Provider {
public static final String EXCEPTION_MESSAGE = "The attribute %s is not supported for %s.";
private String issuer;
private String name;
private String clientName;
private String clientId;
private String clientSecret;
private Collection<String> scopes;
private UsernameAttribute useAsUsername;
private String logoutUrl;
private String authorizationUri;
private String tokenUri;
private String userInfoUri;
public Provider(
String issuer,
String name,
String clientName,
String clientId,
String clientSecret,
Collection<String> scopes,
UsernameAttribute useAsUsername,
String logoutUrl,
String authorizationUri,
String tokenUri,
String userInfoUri) {
this.issuer = issuer;
this.name = name;
this.clientName = clientName;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.scopes = scopes == null ? new ArrayList<>() : scopes;
this.useAsUsername =
useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL;
this.logoutUrl = logoutUrl;
this.authorizationUri = authorizationUri;
this.tokenUri = tokenUri;
this.userInfoUri = userInfoUri;
}
public void setScopes(String scopes) {
if (scopes != null && !scopes.isBlank()) {
this.scopes =
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
}
}
private UsernameAttribute validateUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (name) {
case "google" -> {
return validateGoogleUsernameAttribute(usernameAttribute);
}
case "github" -> {
return validateGitHubUsernameAttribute(usernameAttribute);
}
case "keycloak" -> {
return validateKeycloakUsernameAttribute(usernameAttribute);
}
default -> {
return usernameAttribute;
}
}
}
private UsernameAttribute validateKeycloakUsernameAttribute(
UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
switch (usernameAttribute) {
case LOGIN, EMAIL, NAME -> {
return usernameAttribute;
}
default ->
throw new UnsupportedUsernameAttribute(
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
}
}
@Override
public String toString() {
return "Provider [name="
+ getName()
+ ", clientName="
+ getClientName()
+ ", clientId="
+ getClientId()
+ ", clientSecret="
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
+ ", scopes="
+ getScopes()
+ ", useAsUsername="
+ getUseAsUsername()
+ "]";
}
}

View File

@@ -84,8 +84,8 @@ public class CertificateValidationService {
Enumeration<String> aliases = trustStore.aliases(); Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) { while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement()); Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) { if (trustCert instanceof X509Certificate x509Cert) {
anchors.add(new TrustAnchor((X509Certificate) trustCert, null)); anchors.add(new TrustAnchor(x509Cert, null));
} }
} }

View File

@@ -49,7 +49,7 @@ public class PostHogService {
} }
private void captureSystemInfo() { private void captureSystemInfo() {
if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) { if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
return; return;
} }
try { try {
@@ -60,7 +60,7 @@ public class PostHogService {
} }
public void captureEvent(String eventName, Map<String, Object> properties) { public void captureEvent(String eventName, Map<String, Object> properties) {
if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) { if (!applicationProperties.getSystem().isAnalyticsEnabled()) {
return; return;
} }
postHog.capture(uniqueId, eventName, properties); postHog.capture(uniqueId, eventName, properties);
@@ -315,7 +315,7 @@ public class PostHogService {
addIfNotEmpty( addIfNotEmpty(
properties, properties,
"system_enableAnalytics", "system_enableAnalytics",
applicationProperties.getSystem().getEnableAnalytics()); applicationProperties.getSystem().isAnalyticsEnabled());
// Capture UI properties // Capture UI properties
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName()); addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());

View File

@@ -49,7 +49,8 @@ public class FileMonitor {
this.pathFilter = pathFilter; this.pathFilter = pathFilter;
this.readyForProcessingFiles = ConcurrentHashMap.newKeySet(); this.readyForProcessingFiles = ConcurrentHashMap.newKeySet();
this.watchService = FileSystems.getDefault().newWatchService(); this.watchService = FileSystems.getDefault().newWatchService();
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath()).toAbsolutePath(); log.info("Monitoring directory: {}", runtimePathConfig.getPipelineWatchedFoldersPath());
this.rootDir = Path.of(runtimePathConfig.getPipelineWatchedFoldersPath());
} }
private boolean shouldNotProcess(Path path) { private boolean shouldNotProcess(Path path) {

View File

@@ -169,7 +169,7 @@ public class FileToPdf {
} }
} }
// search for the main HTML file. // Search for the main HTML file.
try (Stream<Path> walk = Files.walk(tempDirectory)) { try (Stream<Path> walk = Files.walk(tempDirectory)) {
List<Path> htmlFiles = List<Path> htmlFiles =
walk.filter(file -> file.toString().endsWith(".html")) walk.filter(file -> file.toString().endsWith(".html"))
@@ -190,46 +190,20 @@ public class FileToPdf {
} }
} }
public static byte[] convertBookTypeToPdf(byte[] bytes, String originalFilename)
throws IOException, InterruptedException {
if (originalFilename == null || originalFilename.lastIndexOf('.') == -1) {
throw new IllegalArgumentException("Invalid original filename.");
}
String fileExtension = originalFilename.substring(originalFilename.lastIndexOf('.'));
List<String> command = new ArrayList<>();
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
Path tempInputFile = null;
try {
// Create temp file with appropriate extension
tempInputFile = Files.createTempFile("input_", fileExtension);
Files.write(tempInputFile, bytes);
command.add("ebook-convert");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command);
return Files.readAllBytes(tempOutputFile);
} finally {
// Clean up temporary files
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
}
Files.deleteIfExists(tempOutputFile);
}
}
static String sanitizeZipFilename(String entryName) { static String sanitizeZipFilename(String entryName) {
if (entryName == null || entryName.trim().isEmpty()) { if (entryName == null || entryName.trim().isEmpty()) {
return entryName; return "";
} }
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", "");
entryName = entryName.replaceAll("^[\\\\/]+", "");
// Recursively remove path traversal sequences
while (entryName.contains("../") || entryName.contains("..\\")) { while (entryName.contains("../") || entryName.contains("..\\")) {
entryName = entryName.replace("../", "").replace("..\\", ""); entryName = entryName.replace("../", "").replace("..\\", "");
} }
// Normalize all backslashes to forward slashes
entryName = entryName.replaceAll("\\\\", "/");
return entryName; return entryName;
} }
} }

View File

@@ -9,15 +9,10 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -30,6 +25,7 @@ import io.github.pixee.security.Urls;
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.YamlHelper;
@Slf4j @Slf4j
public class GeneralUtils { public class GeneralUtils {
@@ -338,218 +334,16 @@ public class GeneralUtils {
} }
} }
public static void saveKeyToConfig(String id, String key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, boolean key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException {
doSaveKeyToConfig(id, (key == null ? "" : key), autoGenerated);
}
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
throws IOException {
doSaveKeyToConfig(id, String.valueOf(key), autoGenerated);
}
/*------------------------------------------------------------------------* /*------------------------------------------------------------------------*
* Internal Implementation Details * * Internal Implementation Details *
*------------------------------------------------------------------------*/ *------------------------------------------------------------------------*/
/** public static void saveKeyToSettings(String key, Object newValue) throws IOException {
* Actually performs the line-based update for the given path (e.g. "security.csrfDisabled") to String[] keyArray = key.split("\\.");
* a new string value (e.g. "true"), possibly marking it as auto-generated.
*/
private static void doSaveKeyToConfig(String fullPath, String newValue, boolean autoGenerated)
throws IOException {
// 1) Load the file (settings.yml)
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
if (!Files.exists(settingsPath)) { YamlHelper settingsYaml = new YamlHelper(settingsPath);
log.warn("Settings file not found at {}, creating a new empty file...", settingsPath); settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
Files.createDirectories(settingsPath.getParent()); settingsYaml.saveOverride(settingsPath);
Files.createFile(settingsPath);
}
List<String> lines = Files.readAllLines(settingsPath);
// 2) Build a map of "nestedKeyPath -> lineIndex" by parsing indentation
// Also track each line's indentation so we can preserve it when rewriting.
Map<String, LineInfo> pathToLine = parseNestedYamlKeys(lines);
// 3) If the path is found, rewrite its line. Else, append at the bottom (no indentation).
boolean changed = false;
if (pathToLine.containsKey(fullPath)) {
// Rewrite existing line
LineInfo info = pathToLine.get(fullPath);
String oldLine = lines.get(info.lineIndex);
String newLine =
rewriteLine(oldLine, info.indentSpaces, fullPath, newValue, autoGenerated);
if (!newLine.equals(oldLine)) {
lines.set(info.lineIndex, newLine);
changed = true;
}
} else {
// Append a new line at the bottom, with zero indentation
String appended = fullPath + ": " + newValue;
if (autoGenerated) {
appended += " # Automatically Generated Settings (Do Not Edit Directly)";
}
lines.add(appended);
changed = true;
}
// 4) If changed, write back to file
if (changed) {
Files.write(settingsPath, lines);
log.info(
"Updated '{}' to '{}' (autoGenerated={}) in {}",
fullPath,
newValue,
autoGenerated,
settingsPath);
} else {
log.info("No changes for '{}' (already set to '{}').", fullPath, newValue);
}
}
/** A small record-like class that holds: - lineIndex - indentSpaces */
private static class LineInfo {
int lineIndex;
int indentSpaces;
public LineInfo(int lineIndex, int indentSpaces) {
this.lineIndex = lineIndex;
this.indentSpaces = indentSpaces;
}
}
/**
* Parse the YAML lines to build a map: "full.nested.key" -> (lineIndex, indentSpaces). We do a
* naive indentation-based path stacking: - 2 spaces = 1 indent level - lines that start with
* fewer or equal indentation pop the stack - lines that look like "key:" or "key: value" cause
* a push
*/
private static Map<String, LineInfo> parseNestedYamlKeys(List<String> lines) {
Map<String, LineInfo> result = new HashMap<>();
// We'll maintain a stack of (keyName, indentLevel).
// Each line that looks like "myKey:" or "myKey: value" is a new "child" of the top of the
// stack if indent is deeper.
Deque<String> pathStack = new ArrayDeque<>();
Deque<Integer> indentStack = new ArrayDeque<>();
indentStack.push(-1); // sentinel
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
String trimmed = line.trim();
// skip blank lines, comment lines, or list items
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
continue;
}
// check if there's a colon
int colonIdx = trimmed.indexOf(':');
if (colonIdx <= 0) { // must have at least one char before ':'
continue;
}
// parse out key
String keyPart = trimmed.substring(0, colonIdx).trim();
if (keyPart.isEmpty()) {
continue;
}
// count leading spaces for indentation
int leadingSpaces = countLeadingSpaces(line);
int indentLevel = leadingSpaces / 2; // assume 2 spaces per level
// pop from stack until we get to a shallower indentation
while (indentStack.peek() != null && indentStack.peek() >= indentLevel) {
indentStack.pop();
pathStack.pop();
}
// push the new key
pathStack.push(keyPart);
indentStack.push(indentLevel);
// build the full path
String[] arr = pathStack.toArray(new String[0]);
List<String> reversed = Arrays.asList(arr);
Collections.reverse(reversed);
String fullPath = String.join(".", reversed);
// store line info
result.put(fullPath, new LineInfo(i, leadingSpaces));
}
return result;
}
/**
* Rewrite a single line to set a new value, preserving indentation and (optionally) the
* existing or auto-generated inline comment.
*
* <p>For example, oldLine might be: " csrfDisabled: false # set to 'true' to disable CSRF
* protection" newValue = "true" autoGenerated = false
*
* <p>We'll produce something like: " csrfDisabled: true # set to 'true' to disable CSRF
* protection"
*/
private static String rewriteLine(
String oldLine, int indentSpaces, String path, String newValue, boolean autoGenerated) {
// We'll keep the exact leading indentation (indentSpaces).
// Then "key: newValue". We'll try to preserve any existing inline comment unless
// autoGenerated is true.
// 1) Extract leading spaces from the old line (just in case they differ from indentSpaces).
int actualLeadingSpaces = countLeadingSpaces(oldLine);
String leading = oldLine.substring(0, actualLeadingSpaces);
// 2) Remove leading spaces from the rest
String trimmed = oldLine.substring(actualLeadingSpaces);
// 3) Check for existing comment
int hashIndex = trimmed.indexOf('#');
String lineWithoutComment =
(hashIndex >= 0) ? trimmed.substring(0, hashIndex).trim() : trimmed.trim();
String oldComment = (hashIndex >= 0) ? trimmed.substring(hashIndex).trim() : "";
// 4) Rebuild "key: newValue"
// The "key" here is everything before ':' in lineWithoutComment
int colonIdx = lineWithoutComment.indexOf(':');
String existingKey =
(colonIdx >= 0)
? lineWithoutComment.substring(0, colonIdx).trim()
: path; // fallback if line is malformed
StringBuilder sb = new StringBuilder();
sb.append(leading); // restore original leading spaces
// "key: newValue"
sb.append(existingKey).append(": ").append(newValue);
// 5) If autoGenerated, add/replace comment
if (autoGenerated) {
sb.append(" # Automatically Generated Settings (Do Not Edit Directly)");
} else {
// preserve the old comment if it exists
if (!oldComment.isEmpty()) {
sb.append(" ").append(oldComment);
}
}
return sb.toString();
}
private static int countLeadingSpaces(String line) {
int count = 0;
for (char c : line.toCharArray()) {
if (c == ' ') count++;
else break;
}
return count;
} }
public static String generateMachineFingerprint() { public static String generateMachineFingerprint() {

View File

@@ -49,10 +49,10 @@ public class ImageProcessingUtils {
public static byte[] getImageData(BufferedImage image) { public static byte[] getImageData(BufferedImage image) {
DataBuffer dataBuffer = image.getRaster().getDataBuffer(); DataBuffer dataBuffer = image.getRaster().getDataBuffer();
if (dataBuffer instanceof DataBufferByte) { if (dataBuffer instanceof DataBufferByte dataBufferByte) {
return ((DataBufferByte) dataBuffer).getData(); return dataBufferByte.getData();
} else if (dataBuffer instanceof DataBufferInt) { } else if (dataBuffer instanceof DataBufferInt dataBufferInt) {
int[] intData = ((DataBufferInt) dataBuffer).getData(); int[] intData = dataBufferInt.getData();
ByteBuffer byteBuffer = ByteBuffer.allocate(intData.length * 4); ByteBuffer byteBuffer = ByteBuffer.allocate(intData.length * 4);
byteBuffer.asIntBuffer().put(intData); byteBuffer.asIntBuffer().put(intData);
return byteBuffer.array(); return byteBuffer.array();

View File

@@ -1,6 +1,10 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.*; import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -222,7 +226,7 @@ public class ProcessExecutor {
boolean isQpdf = boolean isQpdf =
command != null && !command.isEmpty() && command.get(0).contains("qpdf"); command != null && !command.isEmpty() && command.get(0).contains("qpdf");
if (outputLines.size() > 0) { if (!outputLines.isEmpty()) {
String outputMessage = String.join("\n", outputLines); String outputMessage = String.join("\n", outputLines);
messages += outputMessage; messages += outputMessage;
if (!liveUpdates) { if (!liveUpdates) {
@@ -230,7 +234,7 @@ public class ProcessExecutor {
} }
} }
if (errorLines.size() > 0) { if (!errorLines.isEmpty()) {
String errorMessage = String.join("\n", errorLines); String errorMessage = String.join("\n", errorLines);
messages += errorMessage; messages += errorMessage;
if (!liveUpdates) { if (!liveUpdates) {

View File

@@ -7,8 +7,6 @@ import jakarta.servlet.http.HttpServletRequest;
public class UrlUtils { public class UrlUtils {
private UrlUtils() {}
public static String getOrigin(HttpServletRequest request) { public static String getOrigin(HttpServletRequest request) {
String scheme = request.getScheme(); // http or https String scheme = request.getScheme(); // http or https
String serverName = request.getServerName(); // localhost String serverName = request.getServerName(); // localhost

View File

@@ -0,0 +1,36 @@
package stirling.software.SPDF.utils.validation;
import java.util.Collection;
import stirling.software.SPDF.model.provider.Provider;
public class Validator {
public static boolean validateProvider(Provider provider) {
if (provider == null) {
return false;
}
if (isStringEmpty(provider.getClientId())) {
return false;
}
if (isStringEmpty(provider.getClientSecret())) {
return false;
}
if (isCollectionEmpty(provider.getScopes())) {
return false;
}
return true;
}
public static boolean isStringEmpty(String input) {
return input == null || input.isBlank();
}
public static boolean isCollectionEmpty(Collection<String> input) {
return input == null || input.isEmpty();
}
}

View File

@@ -572,8 +572,8 @@ login.invalid=اسم المستخدم أو كلمة المرور غير صالح
login.locked=تم قفل حسابك. login.locked=تم قفل حسابك.
login.signinTitle=الرجاء تسجيل الدخول login.signinTitle=الرجاء تسجيل الدخول
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
login.oauth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2 login.oAuth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2
login.oauth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول. login.oAuth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول.
login.oauth2RequestNotFound=لم يتم العثور على طلب التفويض login.oauth2RequestNotFound=لم يتم العثور على طلب التفويض
login.oauth2InvalidUserInfoResponse=استجابة معلومات المستخدم غير صالحة login.oauth2InvalidUserInfoResponse=استجابة معلومات المستخدم غير صالحة
login.oauth2invalidRequest=طلب غير صالح login.oauth2invalidRequest=طلب غير صالح
@@ -951,6 +951,7 @@ fileToPDF.submit=تحويل إلى PDF
compress.title=ضغط compress.title=ضغط
compress.header=ضغط ملف PDF compress.header=ضغط ملف PDF
compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF. compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF.
compress.grayscale.label=تطبيق التدرج الرمادي للضغط
compress.selectText.1=الوضع اليدوي - من 1 إلى 5 compress.selectText.1=الوضع اليدوي - من 1 إلى 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=مستوى التحسين: compress.selectText.2=مستوى التحسين:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=تطبيق التدرج الرمادي للضغط

View File

@@ -572,8 +572,8 @@ login.invalid=Etibarsız istifadəçi adı və ya şifr.
login.locked=Sizin hesabınız kilidlənmişdir. login.locked=Sizin hesabınız kilidlənmişdir.
login.signinTitle=Zəhmət olmasa, daxil olun login.signinTitle=Zəhmət olmasa, daxil olun
login.ssoSignIn=Single Sign-on vasitəsilə daxil olun login.ssoSignIn=Single Sign-on vasitəsilə daxil olun
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir
login.oauth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın. login.oAuth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
login.oauth2RequestNotFound=Təsdiqlənmə sorğusu tapılmadı login.oauth2RequestNotFound=Təsdiqlənmə sorğusu tapılmadı
login.oauth2InvalidUserInfoResponse=Yanlış İstifadəçi Məlumatı Cavabı login.oauth2InvalidUserInfoResponse=Yanlış İstifadəçi Məlumatı Cavabı
login.oauth2invalidRequest=Etibarsız Sorğu login.oauth2invalidRequest=Etibarsız Sorğu
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF-ə Çevir
compress.title=Sıxışdır compress.title=Sıxışdır
compress.header=PDF-i Sıxışdır compress.header=PDF-i Sıxışdır
compress.credit=Bu servis PDF sıxışdırılması/Optimizasiyası üçün Ghostscript istifadə edir. compress.credit=Bu servis PDF sıxışdırılması/Optimizasiyası üçün Ghostscript istifadə edir.
compress.grayscale.label=Sıxma üçün Boz Rəng Tətbiq Edin
compress.selectText.1=Manual Mod - 1-dən 5-ə compress.selectText.1=Manual Mod - 1-dən 5-ə
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimizasiya səviyyəsi: compress.selectText.2=Optimizasiya səviyyəsi:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Sıxma üçün Boz Rəng Tətbiq Edin

View File

@@ -572,8 +572,8 @@ login.invalid=Невалидно потребителско име или пар
login.locked=Вашият акаунт е заключен. login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано login.oAuth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора. login.oAuth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
login.oauth2RequestNotFound=Заявката за оторизация не е намерена login.oauth2RequestNotFound=Заявката за оторизация не е намерена
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
login.oauth2invalidRequest=Невалидна заявка login.oauth2invalidRequest=Невалидна заявка
@@ -951,6 +951,7 @@ fileToPDF.submit=Преобразуване към PDF
compress.title=Компресиране compress.title=Компресиране
compress.header=Компресиране на PDF compress.header=Компресиране на PDF
compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране. compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране.
compress.grayscale.label=Приложи сива скала за компресиране
compress.selectText.1=Ръчен режим - от 1 до 5 compress.selectText.1=Ръчен режим - от 1 до 5
compress.selectText.1.1=При нива на оптимизация от 6 до 9, в допълнение към общото компресиране на PDF, резолюцията на изображението се намалява, за да се намали допълнително размерът на файла. По-високите нива водят до по-силна компресия на изображенията (до 50% от оригиналния размер), като се постига по-голямо намаляване на размера, но с потенциална загуба на качество на изображенията. compress.selectText.1.1=При нива на оптимизация от 6 до 9, в допълнение към общото компресиране на PDF, резолюцията на изображението се намалява, за да се намали допълнително размерът на файла. По-високите нива водят до по-силна компресия на изображенията (до 50% от оригиналния размер), като се постига по-голямо намаляване на размера, но с потенциална загуба на качество на изображенията.
compress.selectText.2=Ниво на оптимизация: compress.selectText.2=Ниво на оптимизация:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Версия
validateSignature.cert.keyUsage=Предназначение на ключа за използване validateSignature.cert.keyUsage=Предназначение на ключа за използване
validateSignature.cert.selfSigned=Самостоятелно подписан validateSignature.cert.selfSigned=Самостоятелно подписан
validateSignature.cert.bits=битове validateSignature.cert.bits=битове
compress.grayscale.label=Приложи сива скала за компресиране

View File

@@ -572,8 +572,8 @@ login.invalid=Nom d'usuari/contrasenya no vàlid
login.locked=Compte bloquejat login.locked=Compte bloquejat
login.signinTitle=Autenticat login.signinTitle=Autenticat
login.ssoSignIn=Inicia sessió mitjançant inici de sessió únic login.ssoSignIn=Inicia sessió mitjançant inici de sessió únic
login.oauth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada login.oAuth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada
login.oauth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador. login.oAuth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador.
login.oauth2RequestNotFound=Sol·licitud d'autorització no trobada login.oauth2RequestNotFound=Sol·licitud d'autorització no trobada
login.oauth2InvalidUserInfoResponse=Resposta d'informació d'usuari no vàlida login.oauth2InvalidUserInfoResponse=Resposta d'informació d'usuari no vàlida
login.oauth2invalidRequest=Sol·licitud no vàlida login.oauth2invalidRequest=Sol·licitud no vàlida
@@ -951,6 +951,7 @@ fileToPDF.submit=Converteix a PDF
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF compress.header=Comprimir PDF
compress.credit=Aquest servei utilitza qpdf per a la compressió/optimització de PDF. compress.credit=Aquest servei utilitza qpdf per a la compressió/optimització de PDF.
compress.grayscale.label=Aplicar escala de grisos per a la compressió
compress.selectText.1=Mode manual: de l'1 al 5 compress.selectText.1=Mode manual: de l'1 al 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Nivell d'optimització: compress.selectText.2=Nivell d'optimització:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplicar escala de grisos per a la compressió

View File

@@ -572,8 +572,8 @@ login.invalid=Neplatné uživatelské jméno nebo heslo.
login.locked=Váš účet byl uzamčen. login.locked=Váš účet byl uzamčen.
login.signinTitle=Prosím přihlaste se login.signinTitle=Prosím přihlaste se
login.ssoSignIn=Přihlásit se přes Single Sign-on login.ssoSignIn=Přihlásit se přes Single Sign-on
login.oauth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno login.oAuth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno
login.oauth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce. login.oAuth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce.
login.oauth2RequestNotFound=Požadavek na autorizaci nebyl nalezen login.oauth2RequestNotFound=Požadavek na autorizaci nebyl nalezen
login.oauth2InvalidUserInfoResponse=Neplatná odpověď s informacemi o uživateli login.oauth2InvalidUserInfoResponse=Neplatná odpověď s informacemi o uživateli
login.oauth2invalidRequest=Neplatný požadavek login.oauth2invalidRequest=Neplatný požadavek
@@ -951,6 +951,7 @@ fileToPDF.submit=Převést na PDF
compress.title=Komprimovat compress.title=Komprimovat
compress.header=Komprimovat PDF compress.header=Komprimovat PDF
compress.credit=Tato služba používá qpdf pro kompresi/optimalizaci PDF. compress.credit=Tato služba používá qpdf pro kompresi/optimalizaci PDF.
compress.grayscale.label=Použít stupnici šedi pro kompresi
compress.selectText.1=Ruční režim - Od 1 do 5 compress.selectText.1=Ruční režim - Od 1 do 5
compress.selectText.1.1=V úrovních optimalizace 6 až 9 je kromě obecné komprese PDF sníženo rozlišení obrázků pro další zmenšení velikosti souboru. Vyšší úrovně vedou k silnější kompresi obrázků (až na 50 % původní velikosti), čímž dosahují většího zmenšení velikosti, ale s potenciální ztrátou kvality obrázků. compress.selectText.1.1=V úrovních optimalizace 6 až 9 je kromě obecné komprese PDF sníženo rozlišení obrázků pro další zmenšení velikosti souboru. Vyšší úrovně vedou k silnější kompresi obrázků (až na 50 % původní velikosti), čímž dosahují většího zmenšení velikosti, ale s potenciální ztrátou kvality obrázků.
compress.selectText.2=Úroveň optimalizace: compress.selectText.2=Úroveň optimalizace:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Verze
validateSignature.cert.keyUsage=Použití klíče validateSignature.cert.keyUsage=Použití klíče
validateSignature.cert.selfSigned=Podepsaný sám sebou validateSignature.cert.selfSigned=Podepsaný sám sebou
validateSignature.cert.bits=bitů validateSignature.cert.bits=bitů
compress.grayscale.label=Použít stupnici šedi pro kompresi

View File

@@ -572,8 +572,8 @@ login.invalid=Ugyldigt brugernavn eller adgangskode.
login.locked=Din konto er blevet låst. login.locked=Din konto er blevet låst.
login.signinTitle=Log venligst ind login.signinTitle=Log venligst ind
login.ssoSignIn=Log ind via Single Sign-on login.ssoSignIn=Log ind via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret
login.oauth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren. login.oAuth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren.
login.oauth2RequestNotFound=Autorisationsanmodning ikke fundet login.oauth2RequestNotFound=Autorisationsanmodning ikke fundet
login.oauth2InvalidUserInfoResponse=Ugyldigt Brugerinfo Svar login.oauth2InvalidUserInfoResponse=Ugyldigt Brugerinfo Svar
login.oauth2invalidRequest=Ugyldig Anmodning login.oauth2invalidRequest=Ugyldig Anmodning
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertér til PDF
compress.title=Komprimer compress.title=Komprimer
compress.header=Komprimer PDF compress.header=Komprimer PDF
compress.credit=Denne tjeneste bruger qpdf til PDF Komprimering/Optimering. compress.credit=Denne tjeneste bruger qpdf til PDF Komprimering/Optimering.
compress.grayscale.label=Anvend gråskala til komprimering
compress.selectText.1=Manuel Tilstand - Fra 1 til 5 compress.selectText.1=Manuel Tilstand - Fra 1 til 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimeringsniveau: compress.selectText.2=Optimeringsniveau:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Anvend gråskala til komprimering

View File

@@ -572,8 +572,8 @@ login.invalid=Benutzername oder Passwort ungültig.
login.locked=Ihr Konto wurde gesperrt. login.locked=Ihr Konto wurde gesperrt.
login.signinTitle=Bitte melden Sie sich an. login.signinTitle=Bitte melden Sie sich an.
login.ssoSignIn=Anmeldung per Single Sign-On login.ssoSignIn=Anmeldung per Single Sign-On
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert login.oAuth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
login.oauth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator. login.oAuth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
login.oauth2invalidRequest=ungültige Anfrage login.oauth2invalidRequest=ungültige Anfrage
@@ -951,6 +951,7 @@ fileToPDF.submit=In PDF konvertieren
compress.title=Komprimieren compress.title=Komprimieren
compress.header=PDF komprimieren compress.header=PDF komprimieren
compress.credit=Dieser Dienst verwendet qpdf für die PDF-Komprimierung/-Optimierung. compress.credit=Dieser Dienst verwendet qpdf für die PDF-Komprimierung/-Optimierung.
compress.grayscale.label=Graustufen für Komprimierung anwenden
compress.selectText.1=Manueller Modus Von 1 bis 5 compress.selectText.1=Manueller Modus Von 1 bis 5
compress.selectText.1.1=In den Optimierungsstufen 6 bis 9 wird zusätzlich zur allgemeinen PDF-Komprimierung die Bildauflösung reduziert, um die Dateigröße weiter zu verringern. Höhere Stufen führen zu einer stärkeren Bildkomprimierung (bis zu 50 % der Originalgröße), wodurch eine stärkere Größenreduzierung erreicht wird, die jedoch mit einem möglichen Qualitätsverlust der Bilder einhergeht. compress.selectText.1.1=In den Optimierungsstufen 6 bis 9 wird zusätzlich zur allgemeinen PDF-Komprimierung die Bildauflösung reduziert, um die Dateigröße weiter zu verringern. Höhere Stufen führen zu einer stärkeren Bildkomprimierung (bis zu 50 % der Originalgröße), wodurch eine stärkere Größenreduzierung erreicht wird, die jedoch mit einem möglichen Qualitätsverlust der Bilder einhergeht.
compress.selectText.2=Optimierungsstufe: compress.selectText.2=Optimierungsstufe:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Schlüsselverwendung validateSignature.cert.keyUsage=Schlüsselverwendung
validateSignature.cert.selfSigned=Selbstsigniert validateSignature.cert.selfSigned=Selbstsigniert
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Graustufen für Komprimierung anwenden

View File

@@ -572,8 +572,8 @@ login.invalid=Μη έγκυρο όνομα χρήστη ή κωδικός.
login.locked=Ο λογαριασμός σας έχει κλειδωθεί. login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
login.signinTitle=Παρακαλώ συνδεθείτε login.signinTitle=Παρακαλώ συνδεθείτε
login.ssoSignIn=Σύνδεση μέσω Single Sign-on login.ssoSignIn=Σύνδεση μέσω Single Sign-on
login.oauth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη login.oAuth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη
login.oauth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή. login.oAuth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή.
login.oauth2RequestNotFound=Το αίτημα εξουσιοδότησης δεν βρέθηκε login.oauth2RequestNotFound=Το αίτημα εξουσιοδότησης δεν βρέθηκε
login.oauth2InvalidUserInfoResponse=Μη έγκυρη απόκριση πληροφοριών χρήστη login.oauth2InvalidUserInfoResponse=Μη έγκυρη απόκριση πληροφοριών χρήστη
login.oauth2invalidRequest=Μη έγκυρο αίτημα login.oauth2invalidRequest=Μη έγκυρο αίτημα
@@ -951,6 +951,7 @@ fileToPDF.submit=Μετατροπή σε PDF
compress.title=Συμπίεση compress.title=Συμπίεση
compress.header=Συμπίεση PDF compress.header=Συμπίεση PDF
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για συμπίεση/βελτιστοποίηση PDF. compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για συμπίεση/βελτιστοποίηση PDF.
compress.grayscale.label=Εφαρμογή κλίμακας του γκρι για συμπίεση
compress.selectText.1=Χειροκίνητη λειτουργία - Από 1 έως 5 compress.selectText.1=Χειροκίνητη λειτουργία - Από 1 έως 5
compress.selectText.1.1=Στα επίπεδα βελτιστοποίησης 6 έως 9, εκτός από τη γενική συμπίεση PDF, η ανάλυση εικόνας μειώνεται για περαιτέρω μείωση του μεγέθους αρχείου. Υψηλότερα επίπεδα οδηγούν σε ισχυρότερη συμπίεση εικόνας (έως και 50% του αρχικού μεγέθους), επιτυγχάνοντας μεγαλύτερη μείωση μεγέθους αλλά με πιθανή απώλεια ποιότητας στις εικόνες. compress.selectText.1.1=Στα επίπεδα βελτιστοποίησης 6 έως 9, εκτός από τη γενική συμπίεση PDF, η ανάλυση εικόνας μειώνεται για περαιτέρω μείωση του μεγέθους αρχείου. Υψηλότερα επίπεδα οδηγούν σε ισχυρότερη συμπίεση εικόνας (έως και 50% του αρχικού μεγέθους), επιτυγχάνοντας μεγαλύτερη μείωση μεγέθους αλλά με πιθανή απώλεια ποιότητας στις εικόνες.
compress.selectText.2=Επίπεδο βελτιστοποίησης: compress.selectText.2=Επίπεδο βελτιστοποίησης:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Έκδοση
validateSignature.cert.keyUsage=Χρήση κλειδιού validateSignature.cert.keyUsage=Χρήση κλειδιού
validateSignature.cert.selfSigned=Αυτο-υπογεγραμμένο validateSignature.cert.selfSigned=Αυτο-υπογεγραμμένο
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Εφαρμογή κλίμακας του γκρι για συμπίεση

View File

@@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
login.locked=Your account has been locked. login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
@@ -951,6 +951,7 @@ fileToPDF.submit=Convert to PDF
compress.title=Compress compress.title=Compress
compress.header=Compress PDF compress.header=Compress PDF
compress.credit=This service uses qpdf for PDF Compress/Optimisation. compress.credit=This service uses qpdf for PDF Compress/Optimisation.
compress.grayscale.label=Apply Grayscale for Compression
compress.selectText.1=Manual Mode - From 1 to 5 compress.selectText.1=Manual Mode - From 1 to 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimisation level: compress.selectText.2=Optimisation level:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Apply Grayscale for Compression

View File

@@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
login.locked=Your account has been locked. login.locked=Your account has been locked.
login.signinTitle=Please sign in login.signinTitle=Please sign in
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
@@ -951,6 +951,7 @@ fileToPDF.submit=Convert to PDF
compress.title=Compress compress.title=Compress
compress.header=Compress PDF compress.header=Compress PDF
compress.credit=This service uses qpdf for PDF Compress/Optimisation. compress.credit=This service uses qpdf for PDF Compress/Optimisation.
compress.grayscale.label=Apply Grayscale for Compression
compress.selectText.1=Manual Mode - From 1 to 5 compress.selectText.1=Manual Mode - From 1 to 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimization level: compress.selectText.2=Optimization level:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Apply Grayscale for Compression

View File

@@ -572,8 +572,8 @@ login.invalid=Nombre de usuario o contraseña erróneos.
login.locked=Su cuenta se ha bloqueado. login.locked=Su cuenta se ha bloqueado.
login.signinTitle=Por favor, inicie sesión login.signinTitle=Por favor, inicie sesión
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
login.oauth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO login.oAuth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
login.oauth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador. login.oAuth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador.
login.oauth2RequestNotFound=Solicitud de autorización no encontrada login.oauth2RequestNotFound=Solicitud de autorización no encontrada
login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida
login.oauth2invalidRequest=Solicitud no válida login.oauth2invalidRequest=Solicitud no válida
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertir a PDF
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF compress.header=Comprimir PDF
compress.credit=Este servicio utiliza qpdf para compresión/optimización de PDF compress.credit=Este servicio utiliza qpdf para compresión/optimización de PDF
compress.grayscale.label=Aplicar escala de grises para compresión
compress.selectText.1=Modo manual - De 1 a 5 compress.selectText.1=Modo manual - De 1 a 5
compress.selectText.1.1=En los niveles de optimización 6 a 9, además de la compresión general de PDF, se reduce la resolución de la imagen para reducir aún más el tamaño del archivo. Los niveles más altos dan como resultado una mayor compresión de la imagen (hasta el 50 % del tamaño original), lo que permite lograr una mayor reducción del tamaño, pero con una posible pérdida de calidad en las imágenes. compress.selectText.1.1=En los niveles de optimización 6 a 9, además de la compresión general de PDF, se reduce la resolución de la imagen para reducir aún más el tamaño del archivo. Los niveles más altos dan como resultado una mayor compresión de la imagen (hasta el 50 % del tamaño original), lo que permite lograr una mayor reducción del tamaño, pero con una posible pérdida de calidad en las imágenes.
compress.selectText.2=Nivel de optimización: compress.selectText.2=Nivel de optimización:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versión
validateSignature.cert.keyUsage=Uso de la llave validateSignature.cert.keyUsage=Uso de la llave
validateSignature.cert.selfSigned=Autofirmado validateSignature.cert.selfSigned=Autofirmado
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplicar escala de grises para compresión

View File

@@ -572,8 +572,8 @@ login.invalid=Okerreko erabiltzaile izena edo pasahitza.
login.locked=Zure kontua blokeatu egin da. login.locked=Zure kontua blokeatu egin da.
login.signinTitle=Mesedez, hasi saioa login.signinTitle=Mesedez, hasi saioa
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago login.oAuth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF bihurtu
compress.title=Konprimatu compress.title=Konprimatu
compress.header=PDFa konprimatu compress.header=PDFa konprimatu
compress.credit=Zerbitzu honek qpdf erabiltzen du PDFak komprimatzeko/optimizatzeko compress.credit=Zerbitzu honek qpdf erabiltzen du PDFak komprimatzeko/optimizatzeko
compress.grayscale.label=Aplikatu grisezko eskala konpresiorako
compress.selectText.1=Eskuz 1etik 5ra compress.selectText.1=Eskuz 1etik 5ra
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimizazio maila: compress.selectText.2=Optimizazio maila:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplikatu grisezko eskala konpresiorako

View File

@@ -572,8 +572,8 @@ login.invalid=نام کاربری یا رمز عبور اشتباه است.
login.locked=حساب شما قفل شده است. login.locked=حساب شما قفل شده است.
login.signinTitle=لطفاً وارد شوید login.signinTitle=لطفاً وارد شوید
login.ssoSignIn=ورود از طریق Single Sign-on login.ssoSignIn=ورود از طریق Single Sign-on
login.oauth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است login.oAuth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است
login.oauth2AdminBlockedUser=ثبت‌نام یا ورود کاربران ثبت‌نشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید. login.oAuth2AdminBlockedUser=ثبت‌نام یا ورود کاربران ثبت‌نشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید.
login.oauth2RequestNotFound=درخواست احراز هویت پیدا نشد login.oauth2RequestNotFound=درخواست احراز هویت پیدا نشد
login.oauth2InvalidUserInfoResponse=پاسخ اطلاعات کاربری نامعتبر است login.oauth2InvalidUserInfoResponse=پاسخ اطلاعات کاربری نامعتبر است
login.oauth2invalidRequest=درخواست نامعتبر login.oauth2invalidRequest=درخواست نامعتبر
@@ -951,6 +951,7 @@ fileToPDF.submit=تبدیل به PDF
compress.title=فشرده‌سازی compress.title=فشرده‌سازی
compress.header=فشرده‌سازی PDF compress.header=فشرده‌سازی PDF
compress.credit=این سرویس از qpdf برای فشرده‌سازی / بهینه‌سازی PDF استفاده می‌کند. compress.credit=این سرویس از qpdf برای فشرده‌سازی / بهینه‌سازی PDF استفاده می‌کند.
compress.grayscale.label=اعمال مقیاس خاکستری برای فشرده‌سازی
compress.selectText.1=حالت دستی - از 1 تا 5 compress.selectText.1=حالت دستی - از 1 تا 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=سطح بهینه‌سازی: compress.selectText.2=سطح بهینه‌سازی:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=نسخه
validateSignature.cert.keyUsage=کاربرد کلید validateSignature.cert.keyUsage=کاربرد کلید
validateSignature.cert.selfSigned=با امضای خود validateSignature.cert.selfSigned=با امضای خود
validateSignature.cert.bits=بیت‌ها validateSignature.cert.bits=بیت‌ها
compress.grayscale.label=اعمال مقیاس خاکستری برای فشرده‌سازی

View File

@@ -572,8 +572,8 @@ login.invalid=Nom d'utilisateur ou mot de passe invalide.
login.locked=Votre compte a été verrouillé. login.locked=Votre compte a été verrouillé.
login.signinTitle=Veuillez vous connecter login.signinTitle=Veuillez vous connecter
login.ssoSignIn=Se connecter via l'authentification unique login.ssoSignIn=Se connecter via l'authentification unique
login.oauth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée login.oAuth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
login.oauth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur. login.oAuth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur.
login.oauth2RequestNotFound=Demande d'autorisation introuvable login.oauth2RequestNotFound=Demande d'autorisation introuvable
login.oauth2InvalidUserInfoResponse=Réponse contenant les informations de l'utilisateur est invalide login.oauth2InvalidUserInfoResponse=Réponse contenant les informations de l'utilisateur est invalide
login.oauth2invalidRequest=Requête invalide login.oauth2invalidRequest=Requête invalide
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertir
compress.title=Compresser un PDF compress.title=Compresser un PDF
compress.header=Compresser un PDF (lorsque c'est possible!) compress.header=Compresser un PDF (lorsque c'est possible!)
compress.credit=Ce service utilise qpdf pour la compression et l'optimisation des PDF. compress.credit=Ce service utilise qpdf pour la compression et l'optimisation des PDF.
compress.grayscale.label=Appliquer l'échelle de gris pour la compression
compress.selectText.1=Mode manuel de 1 à 5 compress.selectText.1=Mode manuel de 1 à 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Niveau d'optimisation compress.selectText.2=Niveau d'optimisation
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Usage de la clé validateSignature.cert.keyUsage=Usage de la clé
validateSignature.cert.selfSigned=Auto-signé validateSignature.cert.selfSigned=Auto-signé
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Appliquer l'échelle de gris pour la compression

View File

@@ -572,8 +572,8 @@ login.invalid=Ainm úsáideora nó pasfhocal neamhbhailí.
login.locked=Tá do chuntas glasáilte. login.locked=Tá do chuntas glasáilte.
login.signinTitle=Sínigh isteach le do thoil login.signinTitle=Sínigh isteach le do thoil
login.ssoSignIn=Logáil isteach trí Chlárú Aonair login.ssoSignIn=Logáil isteach trí Chlárú Aonair
login.oauth2AutoCreateDisabled=OAUTH2 Uath-Chruthaigh Úsáideoir faoi Mhíchumas login.oAuth2AutoCreateDisabled=OAUTH2 Uath-Chruthaigh Úsáideoir faoi Mhíchumas
login.oauth2AdminBlockedUser=Tá bac faoi láthair ar chlárú nó logáil isteach úsáideoirí neamhchláraithe. Déan teagmháil leis an riarthóir le do thoil. login.oAuth2AdminBlockedUser=Tá bac faoi láthair ar chlárú nó logáil isteach úsáideoirí neamhchláraithe. Déan teagmháil leis an riarthóir le do thoil.
login.oauth2RequestNotFound=Níor aimsíodh iarratas údaraithe login.oauth2RequestNotFound=Níor aimsíodh iarratas údaraithe
login.oauth2InvalidUserInfoResponse=Freagra Neamhbhailí Faisnéise Úsáideora login.oauth2InvalidUserInfoResponse=Freagra Neamhbhailí Faisnéise Úsáideora
login.oauth2invalidRequest=Iarratas Neamhbhailí login.oauth2invalidRequest=Iarratas Neamhbhailí
@@ -951,6 +951,7 @@ fileToPDF.submit=Tiontaigh go PDF
compress.title=Comhbhrúigh compress.title=Comhbhrúigh
compress.header=Comhbhrúigh PDF compress.header=Comhbhrúigh PDF
compress.credit=Úsáideann an tseirbhís seo qpdf le haghaidh Comhbhrú/Optimization PDF. compress.credit=Úsáideann an tseirbhís seo qpdf le haghaidh Comhbhrú/Optimization PDF.
compress.grayscale.label=Cuir Scála Liath i bhFeidhm le Comhbhrú
compress.selectText.1=Mód Láimhe - Ó 1 go 5 compress.selectText.1=Mód Láimhe - Ó 1 go 5
compress.selectText.1.1=I leibhéil optamaithe 6 go 9, chomh maith le comhbhrú ginearálta PDF, déantar réiteach íomhá a laghdú de réir scála chun méid comhaid a laghdú tuilleadh. Mar thoradh ar leibhéil níos airde tá comhbhrú íomhá níos láidre (suas le 50% den mhéid bunaidh), ag baint amach laghdú méide níos mó ach le caillteanas cáilíochta féideartha in íomhánna. compress.selectText.1.1=I leibhéil optamaithe 6 go 9, chomh maith le comhbhrú ginearálta PDF, déantar réiteach íomhá a laghdú de réir scála chun méid comhaid a laghdú tuilleadh. Mar thoradh ar leibhéil níos airde tá comhbhrú íomhá níos láidre (suas le 50% den mhéid bunaidh), ag baint amach laghdú méide níos mó ach le caillteanas cáilíochta féideartha in íomhánna.
compress.selectText.2=Leibhéal optamaithe: compress.selectText.2=Leibhéal optamaithe:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Leagan
validateSignature.cert.keyUsage=Úsáid Eochrach validateSignature.cert.keyUsage=Úsáid Eochrach
validateSignature.cert.selfSigned=Féin-Sínithe validateSignature.cert.selfSigned=Féin-Sínithe
validateSignature.cert.bits=giotáin validateSignature.cert.bits=giotáin
compress.grayscale.label=Cuir Scála Liath i bhFeidhm le Comhbhrú

View File

@@ -572,8 +572,8 @@ login.invalid=अमान्य उपयोगकर्ता नाम या
login.locked=आपका खाता लॉक कर दिया गया है। login.locked=आपका खाता लॉक कर दिया गया है।
login.signinTitle=कृपया साइन इन करें login.signinTitle=कृपया साइन इन करें
login.ssoSignIn=सिंगल साइन-ऑन के माध्यम से लॉगिन करें login.ssoSignIn=सिंगल साइन-ऑन के माध्यम से लॉगिन करें
login.oauth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है login.oAuth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है
login.oauth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें। login.oAuth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें।
login.oauth2RequestNotFound=प्राधिकरण अनुरोध नहीं मिला login.oauth2RequestNotFound=प्राधिकरण अनुरोध नहीं मिला
login.oauth2InvalidUserInfoResponse=अमान्य उपयोगकर्ता जानकारी प्रतिक्रिया login.oauth2InvalidUserInfoResponse=अमान्य उपयोगकर्ता जानकारी प्रतिक्रिया
login.oauth2invalidRequest=अमान्य अनुरोध login.oauth2invalidRequest=अमान्य अनुरोध
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF में बदलें
compress.title=कम्प्रेस compress.title=कम्प्रेस
compress.header=PDF कम्प्रेस करें compress.header=PDF कम्प्रेस करें
compress.credit=यह सेवा PDF कम्प्रेस/अनुकूलन के लिए qpdf का उपयोग करती है। compress.credit=यह सेवा PDF कम्प्रेस/अनुकूलन के लिए qpdf का उपयोग करती है।
compress.grayscale.label=संपीड़न के लिए ग्रेस्केल लागू करें
compress.selectText.1=मैनुअल मोड - स्तर 1 से 4 compress.selectText.1=मैनुअल मोड - स्तर 1 से 4
compress.selectText.1.1=अनुकूलन स्तर 6 से 9 में, सामान्य PDF कम्प्रेसन के अतिरिक्त, फ़ाइल आकार को और कम करने के लिए छवि रेज़ोल्यूशन को कम किया जाता है। उच्च स्तर पर छवियों का अधिक कम्प्रेसन होता है (मूल आकार का 50% तक), जिससे आकार में अधिक कमी आती है लेकिन छवियों की गुणवत्ता प्रभावित हो सकती है। compress.selectText.1.1=अनुकूलन स्तर 6 से 9 में, सामान्य PDF कम्प्रेसन के अतिरिक्त, फ़ाइल आकार को और कम करने के लिए छवि रेज़ोल्यूशन को कम किया जाता है। उच्च स्तर पर छवियों का अधिक कम्प्रेसन होता है (मूल आकार का 50% तक), जिससे आकार में अधिक कमी आती है लेकिन छवियों की गुणवत्ता प्रभावित हो सकती है।
compress.selectText.2=अनुकूलन स्तर: compress.selectText.2=अनुकूलन स्तर:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=संस्करण
validateSignature.cert.keyUsage=कुंजी उपयोग validateSignature.cert.keyUsage=कुंजी उपयोग
validateSignature.cert.selfSigned=स्व-हस्ताक्षरित validateSignature.cert.selfSigned=स्व-हस्ताक्षरित
validateSignature.cert.bits=बिट्स validateSignature.cert.bits=बिट्स
compress.grayscale.label=संपीड़न के लिए ग्रेस्केल लागू करें

View File

@@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili zaporka.
login.locked=Vaš račun je zaključan. login.locked=Vaš račun je zaključan.
login.signinTitle=Molimo vas da se prijavite login.signinTitle=Molimo vas da se prijavite
login.ssoSignIn=Prijavite se putem jedinstvene prijave login.ssoSignIn=Prijavite se putem jedinstvene prijave
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oauth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora. login.oAuth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora.
login.oauth2RequestNotFound=Zahtjev za autorizaciju nije pronađen login.oauth2RequestNotFound=Zahtjev za autorizaciju nije pronađen
login.oauth2InvalidUserInfoResponse=Nevažeće informacije o korisniku login.oauth2InvalidUserInfoResponse=Nevažeće informacije o korisniku
login.oauth2invalidRequest=Neispravan zahtjev login.oauth2invalidRequest=Neispravan zahtjev
@@ -951,6 +951,7 @@ fileToPDF.submit=Pretvori u PDF
compress.title=Komprimirajte compress.title=Komprimirajte
compress.header=Komprimirajte PDF compress.header=Komprimirajte PDF
compress.credit=Ova usluga koristi qpdf za komprimiranje / optimizaciju PDF-a. compress.credit=Ova usluga koristi qpdf za komprimiranje / optimizaciju PDF-a.
compress.grayscale.label=Primijeni sivinu za kompresiju
compress.selectText.1=Ručni režim - Od 1 do 5 compress.selectText.1=Ručni režim - Od 1 do 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Nivo optimizacije: compress.selectText.2=Nivo optimizacije:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Primijeni sivinu za kompresiju

View File

@@ -572,8 +572,8 @@ login.invalid=Érvénytelen felhasználónév vagy jelszó.
login.locked=A fiókja zárolva van. login.locked=A fiókja zárolva van.
login.signinTitle=Kérjük, jelentkezzen be login.signinTitle=Kérjük, jelentkezzen be
login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel
login.oauth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva login.oAuth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva
login.oauth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához. login.oAuth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához.
login.oauth2RequestNotFound=A hitelesítési kérés nem található login.oauth2RequestNotFound=A hitelesítési kérés nem található
login.oauth2InvalidUserInfoResponse=Érvénytelen felhasználói információ válasz login.oauth2InvalidUserInfoResponse=Érvénytelen felhasználói információ válasz
login.oauth2invalidRequest=Érvénytelen kérés login.oauth2invalidRequest=Érvénytelen kérés
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertálás PDF-be
compress.title=Tömörítés compress.title=Tömörítés
compress.header=PDF tömörítése compress.header=PDF tömörítése
compress.credit=Ez a szolgáltatás a qpdf használatával végzi a PDF tömörítését/optimalizálását. compress.credit=Ez a szolgáltatás a qpdf használatával végzi a PDF tömörítését/optimalizálását.
compress.grayscale.label=Szürkeárnyalatok alkalmazása tömörítéshez
compress.selectText.1=Kézi mód - 1-től 5-ig compress.selectText.1=Kézi mód - 1-től 5-ig
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimalizálási szint: compress.selectText.2=Optimalizálási szint:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Verzió
validateSignature.cert.keyUsage=Kulcshasználat validateSignature.cert.keyUsage=Kulcshasználat
validateSignature.cert.selfSigned=Önaláírt validateSignature.cert.selfSigned=Önaláírt
validateSignature.cert.bits=bit validateSignature.cert.bits=bit
compress.grayscale.label=Szürkeárnyalatok alkalmazása tömörítéshez

View File

@@ -572,8 +572,8 @@ login.invalid=Nama pengguna atau kata sandi tidak valid.
login.locked=Akun Anda telah dikunci. login.locked=Akun Anda telah dikunci.
login.signinTitle=Silakan masuk login.signinTitle=Silakan masuk
login.ssoSignIn=Masuk melalui Single Sign - on login.ssoSignIn=Masuk melalui Single Sign - on
login.oauth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan login.oAuth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
login.oauth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator. login.oAuth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator.
login.oauth2RequestNotFound=Permintaan otorisasi tidak ditemukan login.oauth2RequestNotFound=Permintaan otorisasi tidak ditemukan
login.oauth2InvalidUserInfoResponse=Respons Info Pengguna Tidak Valid login.oauth2InvalidUserInfoResponse=Respons Info Pengguna Tidak Valid
login.oauth2invalidRequest=Permintaan Tidak Valid login.oauth2invalidRequest=Permintaan Tidak Valid
@@ -951,6 +951,7 @@ fileToPDF.submit=Konversi ke PDF
compress.title=Kompres compress.title=Kompres
compress.header=Kompres PDF compress.header=Kompres PDF
compress.credit=Layanan ini menggunakan qpdf untuk Kompresi/Optimalisasi PDF. compress.credit=Layanan ini menggunakan qpdf untuk Kompresi/Optimalisasi PDF.
compress.grayscale.label=Terapkan Skala Abu-Abu untuk Kompresi
compress.selectText.1=Mode Manual - Dari 1 hingga 5 compress.selectText.1=Mode Manual - Dari 1 hingga 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Tingkat Optimalisasi: compress.selectText.2=Tingkat Optimalisasi:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Terapkan Skala Abu-Abu untuk Kompresi

View File

@@ -262,7 +262,7 @@ home.desc=La tua pagina auto-gestita per modificare qualsiasi PDF.
home.searchBar=Cerca funzionalità... home.searchBar=Cerca funzionalità...
home.viewPdf.title=View/Edit PDF home.viewPdf.title=Visualizza/Modifica PDF
home.viewPdf.desc=Visualizza, annota, aggiungi testo o immagini home.viewPdf.desc=Visualizza, annota, aggiungi testo o immagini
viewPdf.tags=visualizzare,leggere,annotare,testo,immagine viewPdf.tags=visualizzare,leggere,annotare,testo,immagine
@@ -572,8 +572,8 @@ login.invalid=Nome utente o password errati.
login.locked=Il tuo account è stato bloccato. login.locked=Il tuo account è stato bloccato.
login.signinTitle=Per favore accedi login.signinTitle=Per favore accedi
login.ssoSignIn=Accedi tramite Single Sign-on login.ssoSignIn=Accedi tramite Single Sign-on
login.oauth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA login.oAuth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
login.oauth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore. login.oAuth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata
login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
login.oauth2invalidRequest=Richiesta non valida login.oauth2invalidRequest=Richiesta non valida
@@ -951,6 +951,7 @@ fileToPDF.submit=Converti in PDF
compress.title=Comprimi compress.title=Comprimi
compress.header=Comprimi PDF compress.header=Comprimi PDF
compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF. compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF.
compress.grayscale.label=Applica scala di grigio per la compressione
compress.selectText.1=Modalità manuale - Da 1 a 5 compress.selectText.1=Modalità manuale - Da 1 a 5
compress.selectText.1.1=Nei livelli di ottimizzazione da 6 a 9, oltre alla compressione PDF generale, la risoluzione dell'immagine viene ridotta per ridurre ulteriormente le dimensioni del file. Livelli più alti comportano una compressione dell'immagine più forte (fino al 50% delle dimensioni originali), ottenendo una maggiore riduzione delle dimensioni ma con una potenziale perdita di qualità nelle immagini. compress.selectText.1.1=Nei livelli di ottimizzazione da 6 a 9, oltre alla compressione PDF generale, la risoluzione dell'immagine viene ridotta per ridurre ulteriormente le dimensioni del file. Livelli più alti comportano una compressione dell'immagine più forte (fino al 50% delle dimensioni originali), ottenendo una maggiore riduzione delle dimensioni ma con una potenziale perdita di qualità nelle immagini.
compress.selectText.2=Livello di ottimizzazione: compress.selectText.2=Livello di ottimizzazione:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versione
validateSignature.cert.keyUsage=Utilizzo della chiave validateSignature.cert.keyUsage=Utilizzo della chiave
validateSignature.cert.selfSigned=Autofirmato validateSignature.cert.selfSigned=Autofirmato
validateSignature.cert.bits=bit validateSignature.cert.bits=bit
compress.grayscale.label=Applica scala di grigio per la compressione

View File

@@ -572,8 +572,8 @@ login.invalid=ユーザー名かパスワードが無効です。
login.locked=あなたのアカウントはロックされています。 login.locked=あなたのアカウントはロックされています。
login.signinTitle=サインインしてください login.signinTitle=サインインしてください
login.ssoSignIn=シングルサインオンでログイン login.ssoSignIn=シングルサインオンでログイン
login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効 login.oAuth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
login.oauth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。 login.oAuth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。
login.oauth2RequestNotFound=認証リクエストが見つかりません login.oauth2RequestNotFound=認証リクエストが見つかりません
login.oauth2InvalidUserInfoResponse=無効なユーザー情報の応答 login.oauth2InvalidUserInfoResponse=無効なユーザー情報の応答
login.oauth2invalidRequest=無効なリクエスト login.oauth2invalidRequest=無効なリクエスト
@@ -951,6 +951,7 @@ fileToPDF.submit=PDFを変換
compress.title=圧縮 compress.title=圧縮
compress.header=PDFを圧縮 compress.header=PDFを圧縮
compress.credit=本サービスはPDFの圧縮/最適化にqpdfを使用しています。 compress.credit=本サービスはPDFの圧縮/最適化にqpdfを使用しています。
compress.grayscale.label=圧縮にグレースケールを適用する
compress.selectText.1=手動モード - 1から9 compress.selectText.1=手動モード - 1から9
compress.selectText.1.1=最適化レベル69では、一般的なPDF圧縮に加えて画像解像度が縮小され、ファイルサイズがさらに縮小されます。レベルが高くなると、画像圧縮が強化され (元のサイズの最大 50%)、サイズはさらに縮小されますが、画像の品質が低下する可能性があります。 compress.selectText.1.1=最適化レベル69では、一般的なPDF圧縮に加えて画像解像度が縮小され、ファイルサイズがさらに縮小されます。レベルが高くなると、画像圧縮が強化され (元のサイズの最大 50%)、サイズはさらに縮小されますが、画像の品質が低下する可能性があります。
compress.selectText.2=品質レベル: compress.selectText.2=品質レベル:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=バージョン
validateSignature.cert.keyUsage=キーの使用法 validateSignature.cert.keyUsage=キーの使用法
validateSignature.cert.selfSigned=自己署名 validateSignature.cert.selfSigned=自己署名
validateSignature.cert.bits=ビット validateSignature.cert.bits=ビット
compress.grayscale.label=圧縮にグレースケールを適用する

View File

@@ -572,8 +572,8 @@ login.invalid=사용자 이름 또는 비밀번호가 잘못되었습니다.
login.locked=계정이 잠겼습니다. login.locked=계정이 잠겼습니다.
login.signinTitle=로그인해 주세요 login.signinTitle=로그인해 주세요
login.ssoSignIn=단일 로그인으로 로그인 login.ssoSignIn=단일 로그인으로 로그인
login.oauth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다 login.oAuth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다
login.oauth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요. login.oAuth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요.
login.oauth2RequestNotFound=인증 요청을 찾을 수 없습니다 login.oauth2RequestNotFound=인증 요청을 찾을 수 없습니다
login.oauth2InvalidUserInfoResponse=잘못된 사용자 정보 응답 login.oauth2InvalidUserInfoResponse=잘못된 사용자 정보 응답
login.oauth2invalidRequest=잘못된 요청 login.oauth2invalidRequest=잘못된 요청
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF로 변환
compress.title=압축 compress.title=압축
compress.header=PDF 압축 compress.header=PDF 압축
compress.credit=이 서비스는 PDF 압축/최적화를 위해 qpdf를 사용합니다. compress.credit=이 서비스는 PDF 압축/최적화를 위해 qpdf를 사용합니다.
compress.grayscale.label=압축을 위해 그레이스케일 적용
compress.selectText.1=수동 모드 - 1에서 5 compress.selectText.1=수동 모드 - 1에서 5
compress.selectText.1.1=최적화 레벨 6에서 9에서는 일반적인 PDF 압축 외에도 이미지 해상도가 낮아져 파일 크기가 더욱 감소합니다. 높은 레벨은 더 강력한 이미지 압축(원본 크기의 최대 50%)을 초래하여 더 큰 크기 감소를 달성하지만 이미지 품질이 저하될 수 있습니다. compress.selectText.1.1=최적화 레벨 6에서 9에서는 일반적인 PDF 압축 외에도 이미지 해상도가 낮아져 파일 크기가 더욱 감소합니다. 높은 레벨은 더 강력한 이미지 압축(원본 크기의 최대 50%)을 초래하여 더 큰 크기 감소를 달성하지만 이미지 품질이 저하될 수 있습니다.
compress.selectText.2=최적화 레벨: compress.selectText.2=최적화 레벨:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=버전
validateSignature.cert.keyUsage=키 용도 validateSignature.cert.keyUsage=키 용도
validateSignature.cert.selfSigned=자체 서명 validateSignature.cert.selfSigned=자체 서명
validateSignature.cert.bits=비트 validateSignature.cert.bits=비트
compress.grayscale.label=압축을 위해 그레이스케일 적용

View File

@@ -572,8 +572,8 @@ login.invalid=Ongeldige gebruikersnaam of wachtwoord.
login.locked=Je account is geblokkeerd. login.locked=Je account is geblokkeerd.
login.signinTitle=Gelieve in te loggen login.signinTitle=Gelieve in te loggen
login.ssoSignIn=Inloggen via Single Sign-on login.ssoSignIn=Inloggen via Single Sign-on
login.oauth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld login.oAuth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
login.oauth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder. login.oAuth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder.
login.oauth2RequestNotFound=Autorisatieverzoek niet gevonden login.oauth2RequestNotFound=Autorisatieverzoek niet gevonden
login.oauth2InvalidUserInfoResponse=Ongeldige reactie op gebruikersinfo login.oauth2InvalidUserInfoResponse=Ongeldige reactie op gebruikersinfo
login.oauth2invalidRequest=Ongeldig verzoek login.oauth2invalidRequest=Ongeldig verzoek
@@ -951,6 +951,7 @@ fileToPDF.submit=Omzetten naar PDF
compress.title=Comprimeren compress.title=Comprimeren
compress.header=PDF comprimeren compress.header=PDF comprimeren
compress.credit=Deze functie gebruikt qpdf voor PDF Compressie/Optimalisatie. compress.credit=Deze functie gebruikt qpdf voor PDF Compressie/Optimalisatie.
compress.grayscale.label=Grijsschaal toepassen voor compressie
compress.selectText.1=Handmatige modus - Van 1 tot 5 compress.selectText.1=Handmatige modus - Van 1 tot 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimalisatieniveau: compress.selectText.2=Optimalisatieniveau:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Grijsschaal toepassen voor compressie

View File

@@ -572,8 +572,8 @@ login.invalid=Ugyldig brukernavn eller passord.
login.locked=Kontoen din har blitt låst. login.locked=Kontoen din har blitt låst.
login.signinTitle=Vennligst logg inn login.signinTitle=Vennligst logg inn
login.ssoSignIn=Logg inn via Enkel Pålogging login.ssoSignIn=Logg inn via Enkel Pålogging
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Autentiseringsforespørsel ikke funnet login.oauth2RequestNotFound=Autentiseringsforespørsel ikke funnet
login.oauth2InvalidUserInfoResponse=Ugyldig brukerinforespons login.oauth2InvalidUserInfoResponse=Ugyldig brukerinforespons
login.oauth2invalidRequest=Ugyldig forespørsel login.oauth2invalidRequest=Ugyldig forespørsel
@@ -951,6 +951,7 @@ fileToPDF.submit=Konverter til PDF
compress.title=Komprimer compress.title=Komprimer
compress.header=Komprimer PDF compress.header=Komprimer PDF
compress.credit=Denne tjenesten bruker qpdf for PDF-komprimering/optimisering. compress.credit=Denne tjenesten bruker qpdf for PDF-komprimering/optimisering.
compress.grayscale.label=Bruk gråskala for komprimering
compress.selectText.1=Manuell modus - Fra 1 til 5 compress.selectText.1=Manuell modus - Fra 1 til 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimeringsnivå: compress.selectText.2=Optimeringsnivå:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Bruk gråskala for komprimering

View File

@@ -572,8 +572,8 @@ login.invalid=Nieprawidłowe dane logowania
login.locked=Konto jest zablokowane login.locked=Konto jest zablokowane
login.signinTitle=Zaloguj się login.signinTitle=Zaloguj się
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2 login.oAuth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
login.oauth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem. login.oAuth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.oauth2RequestNotFound=Błąd logowania OAuth2 login.oauth2RequestNotFound=Błąd logowania OAuth2
login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania
login.oauth2invalidRequest=Nieprawidłowe żądanie login.oauth2invalidRequest=Nieprawidłowe żądanie
@@ -951,6 +951,7 @@ fileToPDF.submit=Konwertuj na PDF
compress.title=Kompresuj compress.title=Kompresuj
compress.header=Kompresuj PDF compress.header=Kompresuj PDF
compress.credit=Ta usługa używa qpdf do kompresji/optymalizacji PDF. compress.credit=Ta usługa używa qpdf do kompresji/optymalizacji PDF.
compress.grayscale.label=Zastosuj skalę szarości do kompresji
compress.selectText.1=Tryb ręczny - Od 1 do 5 compress.selectText.1=Tryb ręczny - Od 1 do 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Poziom optymalizacji: compress.selectText.2=Poziom optymalizacji:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Zastosuj skalę szarości do kompresji

View File

@@ -572,8 +572,8 @@ login.invalid=Usuário ou senha inválidos.
login.locked=Sua conta foi bloqueada. login.locked=Sua conta foi bloqueada.
login.signinTitle=Por favor, inicie a sessão login.signinTitle=Por favor, inicie a sessão
login.ssoSignIn=Iniciar sessão através de login único (SSO) login.ssoSignIn=Iniciar sessão através de login único (SSO)
login.oauth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado login.oAuth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
login.oauth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador. login.oAuth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
login.oauth2RequestNotFound=Solicitação de autorização não encontrada login.oauth2RequestNotFound=Solicitação de autorização não encontrada
login.oauth2InvalidUserInfoResponse=Resposta de informação de usuário inválida login.oauth2InvalidUserInfoResponse=Resposta de informação de usuário inválida
login.oauth2invalidRequest=Requisição Inválida login.oauth2invalidRequest=Requisição Inválida
@@ -951,6 +951,7 @@ fileToPDF.submit=Converter para PDF
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir compress.header=Comprimir
compress.credit=Este serviço usa o Qpdf para compressão/otimização de PDF. compress.credit=Este serviço usa o Qpdf para compressão/otimização de PDF.
compress.grayscale.label=Aplicar escala de cinza para compressão
compress.selectText.1=Modo Manual - De 1 a 9 compress.selectText.1=Modo Manual - De 1 a 9
compress.selectText.1.1=Nos níveis de otimização 6-9, além da compressão normal do PDF, a resolução das imagens são reduzidas, para diminuir ainda mais o tamanho do arquivo. Quanto maior o nível, maior a compressão da imagem (até 50% do tamanho original), resultando em tamanho menor do arquivo, porém com menor qualidade nas imagens. compress.selectText.1.1=Nos níveis de otimização 6-9, além da compressão normal do PDF, a resolução das imagens são reduzidas, para diminuir ainda mais o tamanho do arquivo. Quanto maior o nível, maior a compressão da imagem (até 50% do tamanho original), resultando em tamanho menor do arquivo, porém com menor qualidade nas imagens.
compress.selectText.2=Nível de Otimização: compress.selectText.2=Nível de Otimização:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versão
validateSignature.cert.keyUsage=Uso da chave validateSignature.cert.keyUsage=Uso da chave
validateSignature.cert.selfSigned=Autoassinados validateSignature.cert.selfSigned=Autoassinados
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplicar escala de cinza para compressão

View File

@@ -572,8 +572,8 @@ login.invalid=Nome de utilizador ou palavra-passe inválidos.
login.locked=A sua conta foi bloqueada. login.locked=A sua conta foi bloqueada.
login.signinTitle=Por favor inicie sessão login.signinTitle=Por favor inicie sessão
login.ssoSignIn=Login via Single Sign-on login.ssoSignIn=Login via Single Sign-on
login.oauth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada login.oAuth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada
login.oauth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador. login.oAuth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador.
login.oauth2RequestNotFound=Pedido de autorização não encontrado login.oauth2RequestNotFound=Pedido de autorização não encontrado
login.oauth2InvalidUserInfoResponse=Resposta de Informação de Utilizador Inválida login.oauth2InvalidUserInfoResponse=Resposta de Informação de Utilizador Inválida
login.oauth2invalidRequest=Pedido Inválido login.oauth2invalidRequest=Pedido Inválido
@@ -951,6 +951,7 @@ fileToPDF.submit=Converter para PDF
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF compress.header=Comprimir PDF
compress.credit=Este serviço usa qpdf para Compressão/Otimização de PDF. compress.credit=Este serviço usa qpdf para Compressão/Otimização de PDF.
compress.grayscale.label=Aplicar escala de cinzentos para compressão
compress.selectText.1=Modo Manual - De 1 a 5 compress.selectText.1=Modo Manual - De 1 a 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Nível de otimização: compress.selectText.2=Nível de otimização:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versão
validateSignature.cert.keyUsage=Utilização da Chave validateSignature.cert.keyUsage=Utilização da Chave
validateSignature.cert.selfSigned=Auto-Assinado validateSignature.cert.selfSigned=Auto-Assinado
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplicar escala de cinzentos para compressão

View File

@@ -572,8 +572,8 @@ login.invalid=Nume de utilizator sau parolă invalidă.
login.locked=Contul tău a fost blocat. login.locked=Contul tău a fost blocat.
login.signinTitle=Te rugăm să te autentifici login.signinTitle=Te rugăm să te autentifici
login.ssoSignIn=Conectare prin conectare unică login.ssoSignIn=Conectare prin conectare unică
login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată login.oAuth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
login.oauth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul. login.oAuth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul.
login.oauth2RequestNotFound=Cererea de autorizare nu a fost găsită login.oauth2RequestNotFound=Cererea de autorizare nu a fost găsită
login.oauth2InvalidUserInfoResponse=Răspuns Invalid la Informațiile Utilizatorului login.oauth2InvalidUserInfoResponse=Răspuns Invalid la Informațiile Utilizatorului
login.oauth2invalidRequest=Cerere Invalidă login.oauth2invalidRequest=Cerere Invalidă
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertiți în PDF
compress.title=Comprimare compress.title=Comprimare
compress.header=Comprimare PDF compress.header=Comprimare PDF
compress.credit=Acest serviciu utilizează qpdf pentru comprimarea/optimizarea PDF-urilor. compress.credit=Acest serviciu utilizează qpdf pentru comprimarea/optimizarea PDF-urilor.
compress.grayscale.label=Aplicare scală de gri pentru compresie
compress.selectText.1=Modul manual - de la 1 la 5 compress.selectText.1=Modul manual - de la 1 la 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Nivel de optimizare: compress.selectText.2=Nivel de optimizare:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Aplicare scală de gri pentru compresie

View File

@@ -572,8 +572,8 @@ login.invalid=Неверное имя пользователя или парол
login.locked=Ваша учетная запись заблокирована. login.locked=Ваша учетная запись заблокирована.
login.signinTitle=Пожалуйста, войдите login.signinTitle=Пожалуйста, войдите
login.ssoSignIn=Вход через единый вход login.ssoSignIn=Вход через единый вход
login.oauth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено login.oAuth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено
login.oauth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору. login.oAuth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору.
login.oauth2RequestNotFound=Запрос авторизации не найден login.oauth2RequestNotFound=Запрос авторизации не найден
login.oauth2InvalidUserInfoResponse=Недействительный ответ с информацией о пользователе login.oauth2InvalidUserInfoResponse=Недействительный ответ с информацией о пользователе
login.oauth2invalidRequest=Недействительный запрос login.oauth2invalidRequest=Недействительный запрос
@@ -951,6 +951,7 @@ fileToPDF.submit=Преобразовать в PDF
compress.title=Сжать compress.title=Сжать
compress.header=Сжать PDF compress.header=Сжать PDF
compress.credit=Этот сервис использует qpdf для сжатия/оптимизации PDF. compress.credit=Этот сервис использует qpdf для сжатия/оптимизации PDF.
compress.grayscale.label=Применить шкалу серого для сжатия
compress.selectText.1=Ручной режим - от 1 до 5 compress.selectText.1=Ручной режим - от 1 до 5
compress.selectText.1.1=На уровнях оптимизации от 6 до 9, помимо общего сжатия PDF, разрешение изображений уменьшается для дальнейшего сокращения размера файла. Более высокие уровни приводят к более сильному сжатию изображений (до 50% от исходного размера), обеспечивая большее уменьшение размера, но с возможной потерей качества изображений. compress.selectText.1.1=На уровнях оптимизации от 6 до 9, помимо общего сжатия PDF, разрешение изображений уменьшается для дальнейшего сокращения размера файла. Более высокие уровни приводят к более сильному сжатию изображений (до 50% от исходного размера), обеспечивая большее уменьшение размера, но с возможной потерей качества изображений.
compress.selectText.2=Уровень оптимизации: compress.selectText.2=Уровень оптимизации:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Версия
validateSignature.cert.keyUsage=Использование ключа validateSignature.cert.keyUsage=Использование ключа
validateSignature.cert.selfSigned=Самоподписанный validateSignature.cert.selfSigned=Самоподписанный
validateSignature.cert.bits=бит validateSignature.cert.bits=бит
compress.grayscale.label=Применить шкалу серого для сжатия

View File

@@ -572,8 +572,8 @@ login.invalid=Neplatné používateľské meno alebo heslo.
login.locked=Váš účet bol uzamknutý. login.locked=Váš účet bol uzamknutý.
login.signinTitle=Prosím, prihláste sa login.signinTitle=Prosím, prihláste sa
login.ssoSignIn=Prihlásiť sa cez Single Sign-on login.ssoSignIn=Prihlásiť sa cez Single Sign-on
login.oauth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané login.oAuth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertovať do PDF
compress.title=Komprimovať compress.title=Komprimovať
compress.header=Komprimovať PDF compress.header=Komprimovať PDF
compress.credit=Táto služba používa qpdf pre kompresiu/optimalizáciu PDF. compress.credit=Táto služba používa qpdf pre kompresiu/optimalizáciu PDF.
compress.grayscale.label=Použiť odtiene šedej na kompresiu
compress.selectText.1=Manuálny režim - Od 1 do 5 compress.selectText.1=Manuálny režim - Od 1 do 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Úroveň optimalizácie: compress.selectText.2=Úroveň optimalizácie:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Použiť odtiene šedej na kompresiu

View File

@@ -572,8 +572,8 @@ login.invalid=Neveljavno uporabniško ime ali geslo.
login.locked=Vaš račun je bil zaklenjen. login.locked=Vaš račun je bil zaklenjen.
login.signinTitle=Prosim prijavite se login.signinTitle=Prosim prijavite se
login.ssoSignIn=Prijava prek enotne prijave login.ssoSignIn=Prijava prek enotne prijave
login.oauth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno login.oAuth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno
login.oauth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika. login.oAuth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika.
login.oauth2RequestNotFound=Zahteva za avtorizacijo ni bila najdena login.oauth2RequestNotFound=Zahteva za avtorizacijo ni bila najdena
login.oauth2InvalidUserInfoResponse=Neveljaven odgovor z informacijami o uporabniku login.oauth2InvalidUserInfoResponse=Neveljaven odgovor z informacijami o uporabniku
login.oauth2invalidRequest=Neveljavna zahteva login.oauth2invalidRequest=Neveljavna zahteva
@@ -951,6 +951,7 @@ fileToPDF.submit=Pretvori v PDF
compress.title=Stisnite compress.title=Stisnite
compress.header=Stisnite PDF compress.header=Stisnite PDF
compress.credit=Ta storitev uporablja qpdf za stiskanje/optimizacijo PDF. compress.credit=Ta storitev uporablja qpdf za stiskanje/optimizacijo PDF.
compress.grayscale.label=Uporabi sivinsko lestvico za stiskanje
compress.selectText.1=Ročni način - Od 1 do 5 compress.selectText.1=Ročni način - Od 1 do 5
compress.selectText.1.1=Na stopnjah optimizacije od 6 do 9 je poleg splošnega stiskanja PDF ločljivost slike zmanjšana, da se dodatno zmanjša velikost datoteke. Višje ravni povzročijo močnejše stiskanje slike (do 50 % prvotne velikosti), s čimer se doseže večje zmanjšanje velikosti, vendar s potencialno izgubo kakovosti slik. compress.selectText.1.1=Na stopnjah optimizacije od 6 do 9 je poleg splošnega stiskanja PDF ločljivost slike zmanjšana, da se dodatno zmanjša velikost datoteke. Višje ravni povzročijo močnejše stiskanje slike (do 50 % prvotne velikosti), s čimer se doseže večje zmanjšanje velikosti, vendar s potencialno izgubo kakovosti slik.
compress.selectText.2=Raven optimizacije: compress.selectText.2=Raven optimizacije:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Različica
validateSignature.cert.keyUsage=Uporaba ključa validateSignature.cert.keyUsage=Uporaba ključa
validateSignature.cert.selfSigned=Samopodpisano validateSignature.cert.selfSigned=Samopodpisano
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Uporabi sivinsko lestvico za stiskanje

View File

@@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili lozinka.
login.locked=Vaš nalog je zaključan. login.locked=Vaš nalog je zaključan.
login.signinTitle=Molimo vas da se prijavite login.signinTitle=Molimo vas da se prijavite
login.ssoSignIn=Prijavite se putem jedinstvene prijave login.ssoSignIn=Prijavite se putem jedinstvene prijave
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator. login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Authorization request not found login.oauth2RequestNotFound=Authorization request not found
login.oauth2InvalidUserInfoResponse=Invalid User Info Response login.oauth2InvalidUserInfoResponse=Invalid User Info Response
login.oauth2invalidRequest=Invalid Request login.oauth2invalidRequest=Invalid Request
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertuj u PDF
compress.title=Kompresija compress.title=Kompresija
compress.header=Kompresuj PDF compress.header=Kompresuj PDF
compress.credit=Ova usluga koristi qpdf za kompresiju / optimizaciju PDF-a. compress.credit=Ova usluga koristi qpdf za kompresiju / optimizaciju PDF-a.
compress.grayscale.label=Primeni sivinu za kompresiju
compress.selectText.1=Ručni režim - Od 1 do 5 compress.selectText.1=Ručni režim - Od 1 do 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Nivo optimizacije: compress.selectText.2=Nivo optimizacije:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Primeni sivinu za kompresiju

View File

@@ -572,8 +572,8 @@ login.invalid=Ogiltigt användarnamn eller lösenord.
login.locked=Ditt konto har låsts. login.locked=Ditt konto har låsts.
login.signinTitle=Vänligen logga in login.signinTitle=Vänligen logga in
login.ssoSignIn=Logga in via enkel inloggning login.ssoSignIn=Logga in via enkel inloggning
login.oauth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad login.oAuth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad
login.oauth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören. login.oAuth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören.
login.oauth2RequestNotFound=Auktoriseringsbegäran hittades inte login.oauth2RequestNotFound=Auktoriseringsbegäran hittades inte
login.oauth2InvalidUserInfoResponse=Ogiltigt svar på användarinformation login.oauth2InvalidUserInfoResponse=Ogiltigt svar på användarinformation
login.oauth2invalidRequest=Ogiltig begäran login.oauth2invalidRequest=Ogiltig begäran
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertera till PDF
compress.title=Komprimera compress.title=Komprimera
compress.header=Komprimera PDF compress.header=Komprimera PDF
compress.credit=Denna tjänst använder qpdf för PDF-komprimering/optimering. compress.credit=Denna tjänst använder qpdf för PDF-komprimering/optimering.
compress.grayscale.label=Tillämpa gråskala för komprimering
compress.selectText.1=Manuellt läge - Från 1 till 5 compress.selectText.1=Manuellt läge - Från 1 till 5
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images. compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
compress.selectText.2=Optimeringsnivå: compress.selectText.2=Optimeringsnivå:
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits validateSignature.cert.bits=bits
compress.grayscale.label=Tillämpa gråskala för komprimering

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