From e328833f02b1d383afeec787ed2782e4a5d736dc Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 25 Feb 2025 22:24:01 +0100 Subject: [PATCH 01/17] 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)` 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. --- .../config/security/InitialSecuritySetup.java | 9 +++++---- .../SPDF/config/security/UserService.java | 20 ++++++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 261fc307e..9299d4773 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -36,12 +36,13 @@ public class InitialSecuritySetup { @PostConstruct public void init() { try { - if (databaseService.hasBackup()) { - databaseService.importDatabase(); - } if (!userService.hasUsers()) { - initializeAdminUser(); + if (databaseService.hasBackup()) { + databaseService.importDatabase(); + } else { + initializeAdminUser(); + } } userService.migrateOauth2ToSSO(); diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 71c9f7799..e5ecc64e8 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -121,12 +121,14 @@ public class UserService implements UserServiceInterface { } public User addApiKeyToUser(String username) { - Optional user = findByUsernameIgnoreCase(username); - if (user.isPresent()) { - user.get().setApiKey(generateApiKey()); - return userRepository.save(user.get()); + Optional userOpt = findByUsernameIgnoreCase(username); + User user = saveUser(userOpt, generateApiKey()); + try { + 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) { @@ -171,6 +173,14 @@ public class UserService implements UserServiceInterface { saveUser(username, authenticationType, Role.USER.getRoleId()); } + private User saveUser(Optional 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) throws IllegalArgumentException, SQLException, UnsupportedProviderException { if (!isUsernameValid(username)) { From 300011f9b6e9259d7eff1d57e39fa9b94a351ab4 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:27:59 +0100 Subject: [PATCH 02/17] 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. --- src/main/resources/messages_it_IT.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 2dab8f05b..12da5e429 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -262,7 +262,7 @@ home.desc=La tua pagina auto-gestita per modificare qualsiasi PDF. 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 viewPdf.tags=visualizzare,leggere,annotare,testo,immagine From f64d7d42d9b7caa27ff474d6cd0e3ec84067dc4c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:29:06 +0000 Subject: [PATCH 03/17] Bump peter-evans/create-pull-request from 7.0.6 to 7.0.7 (#3051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7.0.6 to 7.0.7.
Release notes

Sourced from peter-evans/create-pull-request's releases.

Create Pull Request v7.0.7

⚙️ Fixes an issue with commit signing where modifications to the same file in multiple commits squash into the first commit.

What's Changed

New Contributors

Full Changelog: https://github.com/peter-evans/create-pull-request/compare/v7.0.6...v7.0.7

Commits
  • dd2324f fix: use showFileAtRefBase64 to read per-commit file contents (#3744)
  • 367180c ci: remove testv5 cmd
  • 25575a1 build: update distribution (#3736)
  • a56e7a5 build(deps): bump @​octokit/core from 6.1.3 to 6.1.4 (#3711)
  • eac17dc build(deps-dev): bump @​types/node from 18.19.75 to 18.19.76 (#3712)
  • a2e685f build(deps): bump @​octokit/plugin-paginate-rest from 11.4.1 to 11.4.2 (#3713)
  • 6cfd146 build(deps-dev): bump eslint-import-resolver-typescript (#3710)
  • b38e8d3 build(deps-dev): bump prettier from 3.5.0 to 3.5.1 (#3709)
  • 8a41570 build: update distribution (#3691)
  • 2e9b4cc build(deps): bump @​octokit/endpoint from 10.1.2 to 10.1.3 (#3700)
  • Additional commits viewable in compare view

[![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) ---
Dependabot commands and options
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 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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/licenses-update.yml | 2 +- .github/workflows/pre_commit.yml | 2 +- .github/workflows/sync_files.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/licenses-update.yml b/.github/workflows/licenses-update.yml index d8c8df0bd..33b3e4f90 100644 --- a/.github/workflows/licenses-update.yml +++ b/.github/workflows/licenses-update.yml @@ -69,7 +69,7 @@ jobs: - name: Create Pull Request id: cpr 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: token: ${{ steps.generate-token.outputs.token }} commit-message: "Update 3rd Party Licenses" diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index c63929723..436655e83 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -61,7 +61,7 @@ jobs: git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV - name: Create Pull Request 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: token: ${{ steps.generate-token.outputs.token }} commit-message: ":file_folder: pre-commit" diff --git a/.github/workflows/sync_files.yml b/.github/workflows/sync_files.yml index 0926df5d9..b4cee7c97 100644 --- a/.github/workflows/sync_files.yml +++ b/.github/workflows/sync_files.yml @@ -103,7 +103,7 @@ jobs: git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "no changes" - 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: token: ${{ steps.generate-token.outputs.token }} commit-message: Update files From a1f7bb3e4aa1ae586630421f85ff142dca5c613a Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 25 Feb 2025 22:31:20 +0100 Subject: [PATCH 04/17] Refactor Path Handling (#3041) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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. --- .../software/SPDF/SPDFApplication.java | 25 +++---- .../SPDF/config/InstallationPathConfig.java | 32 +++++---- .../SPDF/config/RuntimePathConfig.java | 67 +++++++++---------- .../security/database/DatabaseConfig.java | 4 +- .../controller/web/GeneralWebController.java | 6 +- .../software/SPDF/utils/FileMonitor.java | 3 +- 6 files changed, 68 insertions(+), 69 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/SPDFApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java index 61dfaddd0..02b621fa9 100644 --- a/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -83,18 +83,17 @@ public class SPDFApplication { Map propertyFiles = new HashMap<>(); // External config files - log.info("Settings file: {}", InstallationPathConfig.getSettingsPath()); - if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) { - propertyFiles.put( - "spring.config.additional-location", - "file:" + InstallationPathConfig.getSettingsPath()); + String settingsPath = InstallationPathConfig.getSettingsPath(); + log.info("Settings file: {}", settingsPath); + if (Files.exists(Paths.get(settingsPath))) { + propertyFiles.put("spring.config.additional-location", "file:" + settingsPath); } else { - log.warn( - "External configuration file '{}' does not exist.", - InstallationPathConfig.getSettingsPath()); + log.warn("External configuration file '{}' does not exist.", settingsPath); } - if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) { + String customSettingsPath = InstallationPathConfig.getCustomSettingsPath(); + log.info("Custom settings file: {}", customSettingsPath); + if (Files.exists(Paths.get(customSettingsPath))) { String existingLocation = propertyFiles.getOrDefault("spring.config.additional-location", ""); if (!existingLocation.isEmpty()) { @@ -102,11 +101,9 @@ public class SPDFApplication { } propertyFiles.put( "spring.config.additional-location", - existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath()); + existingLocation + "file:" + customSettingsPath); } else { - log.warn( - "Custom configuration file '{}' does not exist.", - InstallationPathConfig.getCustomSettingsPath()); + log.warn("Custom configuration file '{}' does not exist.", customSettingsPath); } Properties finalProps = new Properties(); @@ -128,7 +125,7 @@ public class SPDFApplication { try { Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath())); Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath())); - } catch (Exception e) { + } catch (IOException e) { log.error("Error creating directories: {}", e.getMessage()); } diff --git a/src/main/java/stirling/software/SPDF/config/InstallationPathConfig.java b/src/main/java/stirling/software/SPDF/config/InstallationPathConfig.java index af6076a97..557a152e7 100644 --- a/src/main/java/stirling/software/SPDF/config/InstallationPathConfig.java +++ b/src/main/java/stirling/software/SPDF/config/InstallationPathConfig.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.config; import java.io.File; +import java.nio.file.Paths; import lombok.extern.slf4j.Slf4j; @@ -46,26 +47,29 @@ public class InstallationPathConfig { if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { String os = System.getProperty("os.name").toLowerCase(); if (os.contains("win")) { - return System.getenv("APPDATA") + File.separator + "Stirling-PDF" + File.separator; + return Paths.get( + System.getenv("APPDATA"), // parent path + "Stirling-PDF") + .toString() + + File.separator; } else if (os.contains("mac")) { - return System.getProperty("user.home") - + File.separator - + "Library" - + File.separator - + "Application Support" - + File.separator - + "Stirling-PDF" + return Paths.get( + System.getProperty("user.home"), + "Library", + "Application Support", + "Stirling-PDF") + .toString() + File.separator; } else { - return System.getProperty("user.home") - + File.separator - + ".config" - + File.separator - + "Stirling-PDF" + return Paths.get( + System.getProperty("user.home"), // parent path + ".config", + "Stirling-PDF") + .toString() + File.separator; } } - return "./"; + return "." + File.separator; } public static String getPath() { diff --git a/src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java b/src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java index 3ad42732b..037c1dde3 100644 --- a/src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java +++ b/src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java @@ -1,8 +1,7 @@ package stirling.software.SPDF.config; -import java.io.File; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.Configuration; @@ -33,52 +32,48 @@ public class RuntimePathConfig { this.properties = properties; this.basePath = InstallationPathConfig.getPath(); - String pipelinePath = basePath + "pipeline" + File.separator; - String watchedFoldersPath = pipelinePath + "watchedFolders" + File.separator; - String finishedFoldersPath = pipelinePath + "finishedFolders" + File.separator; - String webUiConfigsPath = pipelinePath + "defaultWebUIConfigs" + File.separator; + this.pipelinePath = Path.of(basePath, "pipeline").toString(); + String defaultWatchedFolders = Path.of(this.pipelinePath, "watchedFolders").toString(); + String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString(); + String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString(); Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline(); - 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 = watchedFoldersPath; - this.pipelineFinishedFoldersPath = finishedFoldersPath; - this.pipelineDefaultWebUiConfigs = webUiConfigsPath; + this.pipelineWatchedFoldersPath = + resolvePath( + defaultWatchedFolders, + pipeline != null ? pipeline.getWatchedFoldersDir() : null); + this.pipelineFinishedFoldersPath = + resolvePath( + defaultFinishedFolders, + pipeline != null ? pipeline.getFinishedFoldersDir() : null); + this.pipelineDefaultWebUiConfigs = + resolvePath( + defaultWebUIConfigs, + pipeline != null ? pipeline.getWebUIConfigsDir() : null); boolean isDocker = isRunningInDocker(); // Initialize Operation paths - String weasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint"; - String unoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert"; + String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint"; + String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert"; - // Check for custom operation paths Operations operations = properties.getSystem().getCustomPaths().getOperations(); - if (operations != null) { - if (!StringUtils.isEmpty(operations.getWeasyprint())) { - weasyPrintPath = operations.getWeasyprint(); - } - if (!StringUtils.isEmpty(operations.getUnoconvert())) { - unoConvertPath = operations.getUnoconvert(); - } - } + this.weasyPrintPath = + resolvePath( + defaultWeasyPrintPath, + operations != null ? operations.getWeasyprint() : null); + this.unoConvertPath = + resolvePath( + defaultUnoConvertPath, + operations != null ? operations.getUnoconvert() : null); + } - // Assign operations final fields - this.weasyPrintPath = weasyPrintPath; - this.unoConvertPath = unoConvertPath; + private String resolvePath(String defaultPath, String customPath) { + return StringUtils.isNotBlank(customPath) ? customPath : defaultPath; } private boolean isRunningInDocker() { - return Files.exists(Paths.get("/.dockerenv")); + return Files.exists(Path.of("/.dockerenv")); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java index 704fd013d..ba4a02f00 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseConfig.java @@ -1,7 +1,5 @@ package stirling.software.SPDF.config.security.database; -import java.io.File; - import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Qualifier; @@ -37,8 +35,8 @@ public class DatabaseConfig { DATASOURCE_DEFAULT_URL = "jdbc:h2:file:" + InstallationPathConfig.getConfigPath() - + File.separator + "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.runningEE = runningEE; } diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index fb6e43ebc..34202cff9 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -230,7 +230,11 @@ public class GeneralWebController { // Extract font names from external directory fontNames.addAll( getFontNamesFromLocation( - "file:" + InstallationPathConfig.getStaticPath() + "fonts/*")); + "file:" + + InstallationPathConfig.getStaticPath() + + "fonts" + + File.separator + + "*")); return fontNames; } diff --git a/src/main/java/stirling/software/SPDF/utils/FileMonitor.java b/src/main/java/stirling/software/SPDF/utils/FileMonitor.java index 6a815d8ce..214f430e4 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileMonitor.java +++ b/src/main/java/stirling/software/SPDF/utils/FileMonitor.java @@ -49,7 +49,8 @@ public class FileMonitor { this.pathFilter = pathFilter; this.readyForProcessingFiles = ConcurrentHashMap.newKeySet(); 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) { From 2ab951e080bf58f024bb5281658ac2abe56ed6a6 Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 25 Feb 2025 22:31:50 +0100 Subject: [PATCH 05/17] 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. --- .../security/CustomLogoutSuccessHandler.java | 57 ++++++++++++------- .../config/security/IPRateLimitingFilter.java | 4 +- .../security/UserAuthenticationFilter.java | 26 +++++---- .../SPDF/config/security/UserService.java | 28 ++++----- ...tomOAuth2AuthenticationFailureHandler.java | 6 +- ...tomOAuth2AuthenticationSuccessHandler.java | 8 +-- .../security/oauth2/OAuth2Configuration.java | 4 +- .../security/saml2/CertificateUtils.java | 7 +-- .../CustomSaml2AuthenticatedPrincipal.java | 7 ++- ...stomSaml2AuthenticationSuccessHandler.java | 4 +- .../session/SessionPersistentRegistry.java | 32 +++++------ .../SPDF/controller/api/UserController.java | 16 +++--- .../api/misc/AutoSplitPdfController.java | 17 ++++-- .../api/misc/CompressController.java | 9 +-- .../api/pipeline/PipelineProcessor.java | 14 ++--- .../api/security/CertSignController.java | 17 +++--- .../controller/api/security/GetInfoOnPDF.java | 27 +++------ .../api/security/SanitizeController.java | 12 ++-- .../controller/web/AccountWebController.java | 24 ++++---- .../SPDF/model/provider/GitHubProvider.java | 1 + .../SPDF/model/provider/GoogleProvider.java | 1 + .../SPDF/model/provider/KeycloakProvider.java | 1 + .../service/CertificateValidationService.java | 4 +- .../SPDF/utils/ImageProcessingUtils.java | 8 +-- .../CustomLogoutSuccessHandlerTest.java | 2 +- .../converters/ConvertWebsiteToPdfTest.java | 2 +- .../SPDF/utils/validation/ValidatorTest.java | 2 +- 27 files changed, 175 insertions(+), 165 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java index c455f0ebd..b570d625d 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandler.java @@ -45,12 +45,12 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { throws IOException { if (!response.isCommitted()) { if (authentication != null) { - if (authentication instanceof Saml2Authentication) { + if (authentication instanceof Saml2Authentication samlAuthentication) { // Handle SAML2 logout redirection - getRedirect_saml2(request, response, authentication); - } else if (authentication instanceof OAuth2AuthenticationToken) { + getRedirect_saml2(request, response, samlAuthentication); + } else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) { // Handle OAuth2 logout redirection - getRedirect_oauth2(request, response, authentication); + getRedirect_oauth2(request, response, oAuthToken); } else if (authentication instanceof UsernamePasswordAuthenticationToken) { // Handle Username/Password logout getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); @@ -71,13 +71,14 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { // Redirect for SAML2 authentication logout private void getRedirect_saml2( - HttpServletRequest request, HttpServletResponse response, Authentication authentication) + HttpServletRequest request, + HttpServletResponse response, + Saml2Authentication samlAuthentication) throws IOException { SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); String registrationId = samlConf.getRegistrationId(); - Saml2Authentication samlAuthentication = (Saml2Authentication) authentication; CustomSaml2AuthenticatedPrincipal principal = (CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal(); @@ -115,32 +116,46 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { // Redirect for OAuth2 authentication logout private void getRedirect_oauth2( - HttpServletRequest request, HttpServletResponse response, Authentication authentication) + HttpServletRequest request, + HttpServletResponse response, + OAuth2AuthenticationToken oAuthToken) throws IOException { String registrationId; OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); String path = checkForErrors(request); - if (authentication instanceof OAuth2AuthenticationToken oauthToken) { - registrationId = oauthToken.getAuthorizedClientRegistrationId(); - } else { - registrationId = oauth.getProvider() != null ? oauth.getProvider() : ""; - } - String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path; + registrationId = oAuthToken.getAuthorizedClientRegistrationId(); // Redirect based on OAuth2 provider switch (registrationId.toLowerCase()) { case "keycloak" -> { KeycloakProvider keycloak = oauth.getClient().getKeycloak(); - String logoutUrl = - keycloak.getIssuer() - + "/protocol/openid-connect/logout" - + "?client_id=" - + keycloak.getClientId() - + "&post_logout_redirect_uri=" - + response.encodeRedirectURL(redirectUrl); - log.info("Redirecting to Keycloak logout URL: {}", logoutUrl); + + boolean isKeycloak = !keycloak.getIssuer().isBlank(); + boolean isCustomOAuth = !oauth.getIssuer().isBlank(); + + String logoutUrl = redirectUrl; + + if (isKeycloak) { + 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); } case "github", "google" -> { diff --git a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java index 3599a833f..ed269c4d1 100644 --- a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java @@ -25,8 +25,8 @@ public class IPRateLimitingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - if (request instanceof HttpServletRequest) { - HttpServletRequest httpRequest = (HttpServletRequest) request; + if (request instanceof HttpServletRequest httpServletRequest) { + HttpServletRequest httpRequest = httpServletRequest; String method = httpRequest.getMethod(); String requestURI = httpRequest.getRequestURI(); // Check if the request is for static resources diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index e8fafda74..714096c61 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -123,9 +123,11 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter() .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" - + "Alternatively you can disable authentication if this is unexpected"); + + "Alternatively you can disable authentication if this is" + + " unexpected"); return; } } @@ -141,21 +143,21 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // Extract username and determine the login method Object principal = authentication.getPrincipal(); String username = null; - if (principal instanceof UserDetails) { - username = ((UserDetails) principal).getUsername(); + if (principal instanceof UserDetails detailsUser) { + username = detailsUser.getUsername(); loginMethod = LoginMethod.USERDETAILS; - } else if (principal instanceof OAuth2User) { - username = ((OAuth2User) principal).getName(); + } else if (principal instanceof OAuth2User oAuth2User) { + username = oAuth2User.getName(); loginMethod = LoginMethod.OAUTH2USER; OAUTH2 oAuth = securityProp.getOauth2(); blockRegistration = oAuth != null && oAuth.getBlockRegistration(); - } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - username = ((CustomSaml2AuthenticatedPrincipal) principal).name(); + } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + username = saml2User.name(); loginMethod = LoginMethod.SAML2USER; SAML2 saml2 = securityProp.getSaml2(); blockRegistration = saml2 != null && saml2.getBlockRegistration(); - } else if (principal instanceof String) { - username = (String) principal; + } else if (principal instanceof String stringUser) { + username = stringUser; loginMethod = LoginMethod.STRINGUSER; } @@ -170,8 +172,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { boolean isUserDisabled = userService.isUserDisabled(username); boolean notSsoLogin = - !loginMethod.equals(LoginMethod.OAUTH2USER) - && !loginMethod.equals(LoginMethod.SAML2USER); + !LoginMethod.OAUTH2USER.equals(loginMethod) + && !LoginMethod.SAML2USER.equals(loginMethod); // Block user registration if not allowed by configuration if (blockRegistration && !isUserExists) { diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index e5ecc64e8..61b7c40af 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -385,14 +385,14 @@ public class UserService implements UserServiceInterface { for (Object principal : sessionRegistry.getAllPrincipals()) { for (SessionInformation sessionsInformation : sessionRegistry.getAllSessions(principal, false)) { - if (principal instanceof UserDetails userDetails) { - usernameP = userDetails.getUsername(); + if (principal instanceof UserDetails detailsUser) { + usernameP = detailsUser.getUsername(); } else if (principal instanceof OAuth2User oAuth2User) { usernameP = oAuth2User.getName(); } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { usernameP = saml2User.name(); - } else if (principal instanceof String) { - usernameP = (String) principal; + } else if (principal instanceof String stringUser) { + usernameP = stringUser; } if (usernameP.equalsIgnoreCase(username)) { sessionRegistry.expireSession(sessionsInformation.getSessionId()); @@ -404,17 +404,17 @@ public class UserService implements UserServiceInterface { public String getCurrentUsername() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if (principal instanceof UserDetails) { - return ((UserDetails) principal).getUsername(); - } else if (principal instanceof OAuth2User) { - return ((OAuth2User) principal) - .getAttribute( - applicationProperties.getSecurity().getOauth2().getUseAsUsername()); - } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - return ((CustomSaml2AuthenticatedPrincipal) principal).name(); - } else { - return principal.toString(); + if (principal instanceof UserDetails detailsUser) { + return detailsUser.getUsername(); + } else if (principal instanceof OAuth2User oAuth2User) { + return oAuth2User.getAttribute( + applicationProperties.getSecurity().getOauth2().getUseAsUsername()); + } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + return saml2User.name(); + } else if (principal instanceof String stringUser) { + return stringUser; } + return null; } @Transactional diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationFailureHandler.java index be58ac776..9440a6718 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationFailureHandler.java @@ -42,12 +42,12 @@ public class CustomOAuth2AuthenticationFailureHandler getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked"); return; } - if (exception instanceof OAuth2AuthenticationException) { - OAuth2Error error = ((OAuth2AuthenticationException) exception).getError(); + if (exception instanceof OAuth2AuthenticationException oAuth2Exception) { + OAuth2Error error = oAuth2Exception.getError(); String errorCode = error.getErrorCode(); - if (error.getErrorCode().equals("Password must not be null")) { + if ("Password must not be null".equals(error.getErrorCode())) { errorCode = "userAlreadyExistsWeb"; } diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java index b33efd9ca..4ee49aed4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/CustomOAuth2AuthenticationSuccessHandler.java @@ -47,10 +47,10 @@ public class CustomOAuth2AuthenticationSuccessHandler Object principal = authentication.getPrincipal(); String username = ""; - if (principal instanceof OAuth2User oauthUser) { - username = oauthUser.getName(); - } else if (principal instanceof UserDetails oauthUser) { - username = oauthUser.getUsername(); + if (principal instanceof OAuth2User oAuth2User) { + username = oAuth2User.getName(); + } else if (principal instanceof UserDetails detailsUser) { + username = detailsUser.getUsername(); } // Get the saved request diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java index 764c9533c..afe83bc7f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java @@ -230,7 +230,7 @@ public class OAuth2Configuration { // Add existing OAUTH2 Authorities mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); // Add Authorities from database for existing user, if user is present. - if (authority instanceof OAuth2UserAuthority oauth2Auth) { + if (authority instanceof OAuth2UserAuthority oAuth2Auth) { String useAsUsername = applicationProperties .getSecurity() @@ -238,7 +238,7 @@ public class OAuth2Configuration { .getUseAsUsername(); Optional userOpt = userService.findByUsernameIgnoreCase( - (String) oauth2Auth.getAttributes().get(useAsUsername)); + (String) oAuth2Auth.getAttributes().get(useAsUsername)); if (userOpt.isPresent()) { User user = userOpt.get(); mappedAuthorities.add( diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java index 6788d6716..354e78750 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CertificateUtils.java @@ -40,13 +40,12 @@ public class CertificateUtils { Object object = pemParser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); - if (object instanceof PEMKeyPair) { + if (object instanceof PEMKeyPair keypair) { // Handle traditional RSA private key format - PEMKeyPair keypair = (PEMKeyPair) object; return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo()); - } else if (object instanceof PrivateKeyInfo) { + } else if (object instanceof PrivateKeyInfo keyInfo) { // Handle PKCS#8 format - return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object); + return (RSAPrivateKey) converter.getPrivateKey(keyInfo); } else { throw new IllegalArgumentException( "Unsupported key format: " diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticatedPrincipal.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticatedPrincipal.java index 04a83f4d8..fbcdb31b4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticatedPrincipal.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticatedPrincipal.java @@ -8,7 +8,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; @ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true") -public record CustomSaml2AuthenticatedPrincipal(String name, Map> attributes, String nameId, List sessionIndexes) +public record CustomSaml2AuthenticatedPrincipal( + String name, + Map> attributes, + String nameId, + List sessionIndexes) implements Saml2AuthenticatedPrincipal, Serializable { @Override @@ -20,5 +24,4 @@ public record CustomSaml2AuthenticatedPrincipal(String name, Map> getAttributes() { return this.attributes; } - } diff --git a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java index b7c2256c4..e4e2d88ca 100644 --- a/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/saml2/CustomSaml2AuthenticationSuccessHandler.java @@ -41,8 +41,8 @@ public class CustomSaml2AuthenticationSuccessHandler Object principal = authentication.getPrincipal(); log.debug("Starting SAML2 authentication success handling"); - if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - String username = ((CustomSaml2AuthenticatedPrincipal) principal).name(); + if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2Principal) { + String username = saml2Principal.name(); log.debug("Authenticated principal found for user: {}", username); HttpSession session = request.getSession(false); diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index be3de6f90..18b037164 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -43,14 +43,14 @@ public class SessionPersistentRegistry implements SessionRegistry { List sessionInformations = new ArrayList<>(); String principalName = null; - if (principal instanceof UserDetails) { - principalName = ((UserDetails) principal).getUsername(); - } else if (principal instanceof OAuth2User) { - principalName = ((OAuth2User) principal).getName(); - } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - principalName = ((CustomSaml2AuthenticatedPrincipal) principal).name(); - } else if (principal instanceof String) { - principalName = (String) principal; + if (principal instanceof UserDetails detailsUser) { + principalName = detailsUser.getUsername(); + } else if (principal instanceof OAuth2User oAuth2User) { + principalName = oAuth2User.getName(); + } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + principalName = saml2User.name(); + } else if (principal instanceof String stringUser) { + principalName = stringUser; } if (principalName != null) { @@ -74,14 +74,14 @@ public class SessionPersistentRegistry implements SessionRegistry { public void registerNewSession(String sessionId, Object principal) { String principalName = null; - if (principal instanceof UserDetails) { - principalName = ((UserDetails) principal).getUsername(); - } else if (principal instanceof OAuth2User) { - principalName = ((OAuth2User) principal).getName(); - } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - principalName = ((CustomSaml2AuthenticatedPrincipal) principal).name(); - } else if (principal instanceof String) { - principalName = (String) principal; + if (principal instanceof UserDetails detailsUser) { + principalName = detailsUser.getUsername(); + } else if (principal instanceof OAuth2User oAuth2User) { + principalName = oAuth2User.getName(); + } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + principalName = saml2User.name(); + } else if (principal instanceof String stringUser) { + principalName = stringUser; } if (principalName != null) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 9af61553c..3a9fd3c23 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -297,14 +297,14 @@ public class UserController { for (Object principal : principals) { List sessionsInformation = sessionRegistry.getAllSessions(principal, false); - if (principal instanceof UserDetails) { - userNameP = ((UserDetails) principal).getUsername(); - } else if (principal instanceof OAuth2User) { - userNameP = ((OAuth2User) principal).getName(); - } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { - userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).name(); - } else if (principal instanceof String) { - userNameP = (String) principal; + if (principal instanceof UserDetails detailsUser) { + userNameP = detailsUser.getUsername(); + } else if (principal instanceof OAuth2User oAuth2User) { + userNameP = oAuth2User.getName(); + } else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + userNameP = saml2User.name(); + } else if (principal instanceof String stringUser) { + userNameP = stringUser; } if (userNameP.equalsIgnoreCase(username)) { for (SessionInformation sessionInfo : sessionsInformation) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index 4700284cb..50e7032ce 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -61,8 +61,8 @@ public class AutoSplitPdfController { private static String decodeQRCode(BufferedImage bufferedImage) { LuminanceSource source; - if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { - byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); + if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte dataBufferByte) { + byte[] pixels = dataBufferByte.getData(); source = new PlanarYUVLuminanceSource( pixels, @@ -73,8 +73,9 @@ public class AutoSplitPdfController { bufferedImage.getWidth(), bufferedImage.getHeight(), false); - } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { - int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); + } else if (bufferedImage.getRaster().getDataBuffer() + instanceof DataBufferInt dataBufferInt) { + int[] pixels = dataBufferInt.getData(); byte[] newPixels = new byte[pixels.length]; for (int i = 0; i < pixels.length; i++) { newPixels[i] = (byte) (pixels[i] & 0xff); @@ -91,7 +92,8 @@ public class AutoSplitPdfController { false); } else { 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)); @@ -108,7 +110,10 @@ public class AutoSplitPdfController { @Operation( summary = "Auto split PDF pages into separate documents", 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 autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 53ef95cdb..f107f67fc 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -63,8 +63,7 @@ public class CompressController { if (res != null && res.getXObjectNames() != null) { for (COSName name : res.getXObjectNames()) { PDXObject xobj = res.getXObject(name); - if (xobj instanceof PDImageXObject) { - PDImageXObject image = (PDImageXObject) xobj; + if (xobj instanceof PDImageXObject image) { BufferedImage bufferedImage = image.getImage(); int newWidth = (int) (bufferedImage.getWidth() * scaleFactor); @@ -119,7 +118,8 @@ public class CompressController { @Operation( summary = "Optimize PDF file", 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 optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { MultipartFile inputFile = request.getFileInput(); @@ -221,7 +221,8 @@ public class CompressController { // Check if optimized file is larger than the original if (pdfBytes.length > inputFileSize) { 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; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index cc533e4cc..56fe768a0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -118,9 +118,8 @@ public class PipelineProcessor { MultiValueMap body = new LinkedMultiValueMap<>(); body.add("fileInput", file); for (Entry entry : parameters.entrySet()) { - if (entry.getValue() instanceof List) { - List list = (List) entry.getValue(); - for (Object item : list) { + if (entry.getValue() instanceof List entryList) { + for (Object item : entryList) { body.add(entry.getKey(), item); } } else { @@ -139,7 +138,7 @@ public class PipelineProcessor { log.info("Skipping file due to filtering {}", operation); continue; } - if (!response.getStatusCode().equals(HttpStatus.OK)) { + if (!HttpStatus.OK.equals(response.getStatusCode())) { logPrintStream.println("Error: " + response.getBody()); hasErrors = true; continue; @@ -180,9 +179,8 @@ public class PipelineProcessor { body.add("fileInput", file); } for (Entry entry : parameters.entrySet()) { - if (entry.getValue() instanceof List) { - List list = (List) entry.getValue(); - for (Object item : list) { + if (entry.getValue() instanceof List entryList) { + for (Object item : entryList) { body.add(entry.getKey(), item); } } else { @@ -191,7 +189,7 @@ public class PipelineProcessor { } ResponseEntity response = sendWebRequest(url, body); // Handle the response - if (response.getStatusCode().equals(HttpStatus.OK)) { + if (HttpStatus.OK.equals(response.getStatusCode())) { processOutputFiles(operation, response, newOutputFiles); } else { // Log error if the response status is not OK diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 355755996..3a19e9b60 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -129,9 +129,9 @@ public class CertSignController { @Operation( summary = "Sign PDF with a Digital Certificate", description = - "This endpoint accepts a PDF file, a digital certificate and related information to sign" - + " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF" - + " Type:SISO") + "This endpoint accepts a PDF file, a digital certificate and related" + + " information to sign the PDF. It then returns the digitally signed PDF" + + " file. Input:PDF Output:PDF Type:SISO") public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -201,17 +201,14 @@ public class CertSignController { Object pemObject = pemParser.readObject(); JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); PrivateKeyInfo pkInfo; - if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) { + if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo pkcs8EncryptedPrivateKeyInfo) { InputDecryptorProvider decProv = new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); - pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv); - } else if (pemObject instanceof PEMEncryptedKeyPair) { + pkInfo = pkcs8EncryptedPrivateKeyInfo.decryptPrivateKeyInfo(decProv); + } else if (pemObject instanceof PEMEncryptedKeyPair pemEncryptedKeyPair) { PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); - pkInfo = - ((PEMEncryptedKeyPair) pemObject) - .decryptKeyPair(decProv) - .getPrivateKeyInfo(); + pkInfo = pemEncryptedKeyPair.decryptKeyPair(decProv).getPrivateKeyInfo(); } else { pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo(); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index b2bcfb535..1f30bccf8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -214,10 +214,7 @@ public class GetInfoOnPDF { ArrayNode attachmentsArray = objectMapper.createArrayNode(); for (PDPage page : pdfBoxDoc.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationFileAttachment) { - PDAnnotationFileAttachment fileAttachmentAnnotation = - (PDAnnotationFileAttachment) annotation; - + if (annotation instanceof PDAnnotationFileAttachment fileAttachmentAnnotation) { ObjectNode attachmentNode = objectMapper.createObjectNode(); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); attachmentNode.put("Description", fileAttachmentAnnotation.getContents()); @@ -437,9 +434,7 @@ public class GetInfoOnPDF { for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); - if (xObject instanceof PDImageXObject) { - PDImageXObject image = (PDImageXObject) xObject; - + if (xObject instanceof PDImageXObject image) { ObjectNode imageNode = objectMapper.createObjectNode(); imageNode.put("Width", image.getWidth()); imageNode.put("Height", image.getHeight()); @@ -462,10 +457,8 @@ public class GetInfoOnPDF { Set uniqueURIs = new HashSet<>(); // To store unique URIs for (PDAnnotation annotation : annotations) { - if (annotation instanceof PDAnnotationLink) { - PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation; - if (linkAnnotation.getAction() instanceof PDActionURI) { - PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); + if (annotation instanceof PDAnnotationLink linkAnnotation) { + if (linkAnnotation.getAction() instanceof PDActionURI uriAction) { String uri = uriAction.getURI(); uniqueURIs.add(uri); // Add to set to ensure uniqueness } @@ -541,8 +534,7 @@ public class GetInfoOnPDF { Iterable colorSpaceNames = resources.getColorSpaceNames(); for (COSName name : colorSpaceNames) { PDColorSpace colorSpace = resources.getColorSpace(name); - if (colorSpace instanceof PDICCBased) { - PDICCBased iccBased = (PDICCBased) colorSpace; + if (colorSpace instanceof PDICCBased iccBased) { PDStream iccData = iccBased.getPDStream(); byte[] iccBytes = iccData.toByteArray(); @@ -698,12 +690,10 @@ public class GetInfoOnPDF { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { for (Object obj : nodes) { - if (obj instanceof PDStructureNode) { - PDStructureNode node = (PDStructureNode) obj; + if (obj instanceof PDStructureNode node) { ObjectNode elementNode = objectMapper.createObjectNode(); - if (node instanceof PDStructureElement) { - PDStructureElement structureElement = (PDStructureElement) node; + if (node instanceof PDStructureElement structureElement) { elementNode.put("Type", structureElement.getStructureType()); elementNode.put("Content", getContent(structureElement)); @@ -724,8 +714,7 @@ public class GetInfoOnPDF { StringBuilder contentBuilder = new StringBuilder(); for (Object item : structureElement.getKids()) { - if (item instanceof COSString) { - COSString cosString = (COSString) item; + if (item instanceof COSString cosString) { contentBuilder.append(cosString.getString()); } else if (item instanceof PDStructureElement) { // For simplicity, we're handling only COSString and PDStructureElement here diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index bd8904fa2..9b42e23b3 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -44,7 +44,8 @@ public class SanitizeController { @Operation( summary = "Sanitize a PDF file", 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 sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException { MultipartFile inputFile = request.getFileInput(); @@ -103,8 +104,7 @@ public class SanitizeController { for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationWidget) { - PDAnnotationWidget widget = (PDAnnotationWidget) annotation; + if (annotation instanceof PDAnnotationWidget widget) { PDAction action = widget.getAction(); if (action instanceof PDActionJavaScript) { widget.setAction(null); @@ -157,12 +157,12 @@ public class SanitizeController { private void sanitizeLinks(PDDocument document) throws IOException { for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation != null && annotation instanceof PDAnnotationLink) { - PDAction action = ((PDAnnotationLink) annotation).getAction(); + if (annotation != null && annotation instanceof PDAnnotationLink linkAnnotation) { + PDAction action = linkAnnotation.getAction(); if (action != null && (action instanceof PDActionLaunch || action instanceof PDActionURI)) { - ((PDAnnotationLink) annotation).setAction(null); + linkAnnotation.setAction(null); } } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index b979e5184..65e1d055a 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -123,9 +123,7 @@ public class AccountWebController { String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId(); if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) { - return "redirect:" - + request.getRequestURL() - + saml2AuthenticationPath; + return "redirect:" + request.getRequestURL() + saml2AuthenticationPath; } else { providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)"); } @@ -329,21 +327,21 @@ public class AccountWebController { if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { + if (authentication.isAuthenticated()) { Object principal = authentication.getPrincipal(); String username = null; // Retrieve username and other attributes and add login attributes to the model - if (principal instanceof UserDetails userDetails) { - username = userDetails.getUsername(); + if (principal instanceof UserDetails detailsUser) { + username = detailsUser.getUsername(); model.addAttribute("oAuth2Login", false); } - if (principal instanceof OAuth2User userDetails) { - username = userDetails.getName(); + if (principal instanceof OAuth2User oAuth2User) { + username = oAuth2User.getName(); model.addAttribute("oAuth2Login", true); } - if (principal instanceof CustomSaml2AuthenticatedPrincipal userDetails) { - username = userDetails.name(); + if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) { + username = saml2User.name(); model.addAttribute("saml2Login", true); } if (username != null) { @@ -395,10 +393,10 @@ public class AccountWebController { if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { + if (authentication.isAuthenticated()) { Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails userDetails) { - String username = userDetails.getUsername(); + if (principal instanceof UserDetails detailsUser) { + String username = detailsUser.getUsername(); // Fetch user details from the database Optional user = userRepository.findByUsernameIgnoreCase(username); if (user.isEmpty()) { diff --git a/src/main/java/stirling/software/SPDF/model/provider/GitHubProvider.java b/src/main/java/stirling/software/SPDF/model/provider/GitHubProvider.java index 8ca61094f..8d8aaf80c 100644 --- a/src/main/java/stirling/software/SPDF/model/provider/GitHubProvider.java +++ b/src/main/java/stirling/software/SPDF/model/provider/GitHubProvider.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import lombok.NoArgsConstructor; + import stirling.software.SPDF.model.UsernameAttribute; @NoArgsConstructor diff --git a/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java b/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java index 4cf29c402..a8e65c61e 100644 --- a/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java +++ b/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import lombok.NoArgsConstructor; + import stirling.software.SPDF.model.UsernameAttribute; @NoArgsConstructor diff --git a/src/main/java/stirling/software/SPDF/model/provider/KeycloakProvider.java b/src/main/java/stirling/software/SPDF/model/provider/KeycloakProvider.java index 6b89e5b1e..bf2725995 100644 --- a/src/main/java/stirling/software/SPDF/model/provider/KeycloakProvider.java +++ b/src/main/java/stirling/software/SPDF/model/provider/KeycloakProvider.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; import lombok.NoArgsConstructor; + import stirling.software.SPDF.model.UsernameAttribute; @NoArgsConstructor diff --git a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java index 7b4dc6dd8..f8b94f8df 100644 --- a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java +++ b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java @@ -84,8 +84,8 @@ public class CertificateValidationService { Enumeration aliases = trustStore.aliases(); while (aliases.hasMoreElements()) { Object trustCert = trustStore.getCertificate(aliases.nextElement()); - if (trustCert instanceof X509Certificate) { - anchors.add(new TrustAnchor((X509Certificate) trustCert, null)); + if (trustCert instanceof X509Certificate x509Cert) { + anchors.add(new TrustAnchor(x509Cert, null)); } } diff --git a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java index 8bec891cc..f6a496021 100644 --- a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java @@ -49,10 +49,10 @@ public class ImageProcessingUtils { public static byte[] getImageData(BufferedImage image) { DataBuffer dataBuffer = image.getRaster().getDataBuffer(); - if (dataBuffer instanceof DataBufferByte) { - return ((DataBufferByte) dataBuffer).getData(); - } else if (dataBuffer instanceof DataBufferInt) { - int[] intData = ((DataBufferInt) dataBuffer).getData(); + if (dataBuffer instanceof DataBufferByte dataBufferByte) { + return dataBufferByte.getData(); + } else if (dataBuffer instanceof DataBufferInt dataBufferInt) { + int[] intData = dataBufferInt.getData(); ByteBuffer byteBuffer = ByteBuffer.allocate(intData.length * 4); byteBuffer.asIntBuffer().put(intData); return byteBuffer.array(); diff --git a/src/test/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandlerTest.java b/src/test/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandlerTest.java index 904a533d2..37a9f86e6 100644 --- a/src/test/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandlerTest.java +++ b/src/test/java/stirling/software/SPDF/config/security/CustomLogoutSuccessHandlerTest.java @@ -259,4 +259,4 @@ class CustomLogoutSuccessHandlerTest { verify(response).sendRedirect(url + "/login?errorOAuth=" + error); } -} \ No newline at end of file +} diff --git a/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index b2efad0b6..986ca55c2 100644 --- a/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -21,7 +21,7 @@ public class ConvertWebsiteToPdfTest { @Mock private RuntimePathConfig runtimePathConfig; - + private ConvertWebsiteToPDF convertWebsiteToPDF; @BeforeEach diff --git a/src/test/java/stirling/software/SPDF/utils/validation/ValidatorTest.java b/src/test/java/stirling/software/SPDF/utils/validation/ValidatorTest.java index 57b8f1ba2..1e2b075ac 100644 --- a/src/test/java/stirling/software/SPDF/utils/validation/ValidatorTest.java +++ b/src/test/java/stirling/software/SPDF/utils/validation/ValidatorTest.java @@ -51,4 +51,4 @@ class ValidatorTest { ); } -} \ No newline at end of file +} From 4fabc07a4496082b37ba33e54b1dbfeb2990add7 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 25 Feb 2025 21:45:50 +0000 Subject: [PATCH 06/17] 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. --- build.gradle | 2 +- src/main/resources/templates/fragments/navbar.html | 8 ++++---- src/main/resources/templates/view-pdf.html | 4 ---- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index bbfab1d52..2f921fc02 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { } group = "stirling.software" -version = "0.42.0" +version = "0.43.0" java { // 17 is lowest but we support and recommend 21 diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 5503f1ae9..8720b2f72 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -89,12 +89,12 @@ diff --git a/src/main/resources/templates/view-pdf.html b/src/main/resources/templates/view-pdf.html index c9146fc70..8d3836cfd 100644 --- a/src/main/resources/templates/view-pdf.html +++ b/src/main/resources/templates/view-pdf.html @@ -357,10 +357,6 @@ See https://github.com/adobe-type-tools/cmap-resources data-l10n-id="pdfjs-page-input" autocomplete="off"> - - icon - Stirling PDF -
From ac10c9fa4354886f3670585bd3dbc2ce9754fffd Mon Sep 17 00:00:00 2001 From: Ludy Date: Tue, 25 Feb 2025 22:52:59 +0100 Subject: [PATCH 07/17] 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 # --- ## 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> --- build.gradle | 2 +- .../software/SPDF/EE/LicenseKeyChecker.java | 2 +- .../software/SPDF/SPDFApplication.java | 25 +- .../software/SPDF/config/AppConfig.java | 12 +- .../SPDF/config/ConfigInitializer.java | 157 +----- .../software/SPDF/config/InitialSetup.java | 14 +- .../software/SPDF/config/YamlHelper.java | 479 ++++++++++++++++++ .../controller/api/SettingsController.java | 6 +- .../SPDF/model/ApplicationProperties.java | 6 +- .../software/SPDF/service/PostHogService.java | 6 +- .../software/SPDF/utils/GeneralUtils.java | 218 +------- src/main/resources/settings.yml.template | 4 +- 12 files changed, 540 insertions(+), 391 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/YamlHelper.java diff --git a/build.gradle b/build.gradle index 2f921fc02..9f0f4fd85 100644 --- a/build.gradle +++ b/build.gradle @@ -347,8 +347,8 @@ dependencies { // implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion' implementation 'com.coveo:saml-client:5.0.0' - } + implementation 'org.snakeyaml:snakeyaml-engine:2.9' testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" diff --git a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java index 9de7a7059..f87c8a117 100644 --- a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java +++ b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java @@ -51,7 +51,7 @@ public class LicenseKeyChecker { public void updateLicenseKey(String newKey) throws IOException { applicationProperties.getEnterpriseEdition().setKey(newKey); - GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false); + GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey); checkLicense(); } diff --git a/src/main/java/stirling/software/SPDF/SPDFApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java index 02b621fa9..5ac9f663e 100644 --- a/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -83,17 +83,18 @@ public class SPDFApplication { Map propertyFiles = new HashMap<>(); // External config files - String settingsPath = InstallationPathConfig.getSettingsPath(); - log.info("Settings file: {}", settingsPath); - if (Files.exists(Paths.get(settingsPath))) { - propertyFiles.put("spring.config.additional-location", "file:" + settingsPath); + Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); + log.info("Settings file: {}", settingsPath.toString()); + if (Files.exists(settingsPath)) { + propertyFiles.put( + "spring.config.additional-location", "file:" + settingsPath.toString()); } else { - log.warn("External configuration file '{}' does not exist.", settingsPath); + log.warn("External configuration file '{}' does not exist.", settingsPath.toString()); } - String customSettingsPath = InstallationPathConfig.getCustomSettingsPath(); - log.info("Custom settings file: {}", customSettingsPath); - if (Files.exists(Paths.get(customSettingsPath))) { + Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath()); + log.info("Custom settings file: {}", customSettingsPath.toString()); + if (Files.exists(customSettingsPath)) { String existingLocation = propertyFiles.getOrDefault("spring.config.additional-location", ""); if (!existingLocation.isEmpty()) { @@ -101,9 +102,11 @@ public class SPDFApplication { } propertyFiles.put( "spring.config.additional-location", - existingLocation + "file:" + customSettingsPath); + existingLocation + "file:" + customSettingsPath.toString()); } else { - log.warn("Custom configuration file '{}' does not exist.", customSettingsPath); + log.warn( + "Custom configuration file '{}' does not exist.", + customSettingsPath.toString()); } Properties finalProps = new Properties(); @@ -154,7 +157,7 @@ public class SPDFApplication { } else if (os.contains("nix") || os.contains("nux")) { SystemCommand.runCommand(rt, "xdg-open " + url); } - } catch (Exception e) { + } catch (IOException e) { log.error("Error opening browser: {}", e.getMessage()); } } diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 62f82a278..58719f1d0 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -96,9 +96,9 @@ public class AppConfig { @Bean(name = "rateLimit") public boolean rateLimit() { - String appName = System.getProperty("rateLimit"); - if (appName == null) appName = System.getenv("rateLimit"); - return (appName != null) ? Boolean.valueOf(appName) : false; + String rateLimit = System.getProperty("rateLimit"); + if (rateLimit == null) rateLimit = System.getenv("rateLimit"); + return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false; } @Bean(name = "RunningInDocker") @@ -170,16 +170,14 @@ public class AppConfig { @Bean(name = "analyticsPrompt") @Scope("request") public boolean analyticsPrompt() { - return applicationProperties.getSystem().getEnableAnalytics() == null - || "undefined".equals(applicationProperties.getSystem().getEnableAnalytics()); + return applicationProperties.getSystem().getEnableAnalytics() == null; } @Bean(name = "analyticsEnabled") @Scope("request") public boolean analyticsEnabled() { if (applicationProperties.getEnterpriseEdition().isEnabled()) return true; - return applicationProperties.getSystem().getEnableAnalytics() != null - && Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics()); + return applicationProperties.getSystem().isAnalyticsEnabled(); } @Bean(name = "StirlingPDFLabel") diff --git a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java index 092b4fbf4..119fc92b1 100644 --- a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java +++ b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java @@ -9,7 +9,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.*; import lombok.extern.slf4j.Slf4j; @@ -37,7 +36,6 @@ public class ConfigInitializer { log.info("Created settings file from template"); } else { // 2) Merge existing file with the template - Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); URL templateResource = getClass().getClassLoader().getResource("settings.yml.template"); if (templateResource == null) { throw new IOException("Resource not found: settings.yml.template"); @@ -49,160 +47,33 @@ public class ConfigInitializer { Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); } - // 2a) Read lines from both files - List templateLines = Files.readAllLines(tempTemplatePath); - List mainLines = Files.readAllLines(settingsPath); + // Copy setting.yaml to a temp location so we can read lines + Path settingTempPath = Files.createTempFile("settings", ".yaml"); + try (InputStream in = Files.newInputStream(destPath)) { + Files.copy(in, settingTempPath, StandardCopyOption.REPLACE_EXISTING); + } - // 2b) Merge lines - List mergedLines = mergeYamlLinesWithTemplate(templateLines, mainLines); + YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath); + YamlHelper settingsFile = new YamlHelper(settingTempPath); - // 2c) Only write if there's an actual difference - if (!mergedLines.equals(mainLines)) { - Files.write(settingsPath, mergedLines); + boolean changesMade = + settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile); + if (changesMade) { + settingsTemplateFile.save(destPath); log.info("Settings file updated based on template changes."); } else { log.info("No changes detected; settings file left as-is."); } Files.deleteIfExists(tempTemplatePath); + Files.deleteIfExists(settingTempPath); } // 3) Ensure custom settings file exists Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath()); - if (!Files.exists(customSettingsPath)) { + if (Files.notExists(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 mergeYamlLinesWithTemplate( - List templateLines, List mainLines) { - - // 1) Parse template lines into an ordered map: path -> Block - LinkedHashMap templateBlocks = parseYamlBlocks(templateLines); - - // 2) Parse main lines into a map: path -> Block - LinkedHashMap mainBlocks = parseYamlBlocks(mainLines); - - // 3) Build the final list by iterating template blocks in order - List merged = new ArrayList<>(); - for (Map.Entry 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 parseYamlBlocks(List lines) { - LinkedHashMap 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 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 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 lines = new ArrayList<>(); - } } diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java index a533c90a2..4c3131a1c 100644 --- a/src/main/java/stirling/software/SPDF/config/InitialSetup.java +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -44,7 +44,7 @@ public class InitialSetup { if (!GeneralUtils.isValidUUID(uuid)) { // Generating a random UUID as the secret key uuid = UUID.randomUUID().toString(); - GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid); + GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.UUID", uuid); applicationProperties.getAutomaticallyGenerated().setUUID(uuid); } } @@ -54,7 +54,7 @@ public class InitialSetup { if (!GeneralUtils.isValidUUID(secretKey)) { // Generating a random UUID as the secret key secretKey = UUID.randomUUID().toString(); - GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey); + GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.key", secretKey); applicationProperties.getAutomaticallyGenerated().setKey(secretKey); } } @@ -64,8 +64,8 @@ public class InitialSetup { "0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) { Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled(); if (!csrf) { - GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false); - GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false); + GeneralUtils.saveKeyToSettings("security.csrfDisabled", false); + GeneralUtils.saveKeyToSettings("system.enableAnalytics", true); applicationProperties.getSecurity().setCsrfDisabled(false); } } @@ -76,14 +76,14 @@ public class InitialSetup { String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); if (StringUtils.isEmpty(termsUrl)) { String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions"; - GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); + GeneralUtils.saveKeyToSettings("legal.termsAndConditions", defaultTermsUrl); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); } // Initialize Privacy Policy String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); if (StringUtils.isEmpty(privacyUrl)) { String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy"; - GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false); + GeneralUtils.saveKeyToSettings("legal.privacyPolicy", defaultPrivacyUrl); applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); } } @@ -97,7 +97,7 @@ public class InitialSetup { appVersion = props.getProperty("version"); } catch (Exception e) { } + GeneralUtils.saveKeyToSettings("AutomaticallyGenerated.appVersion", appVersion); applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); - GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false); } } diff --git a/src/main/java/stirling/software/SPDF/config/YamlHelper.java b/src/main/java/stirling/software/SPDF/config/YamlHelper.java new file mode 100644 index 000000000..8d1aa2914 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/YamlHelper.java @@ -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 sourceKeys = sourceYaml.getAllKeys(); + Set 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 keys, Object newValue) { + return updateValue(getRootNode(), keys, newValue); + } + + private boolean updateValue(Node node, List keys, Object newValue) { + if (!(node instanceof MappingNode mappingNode)) return false; + + List 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 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 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 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 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 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 getAllKeys(Node node) { + Set 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 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 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 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()); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index 4075a1d9d..dce5bdb26 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -31,14 +31,14 @@ public class SettingsController { @PostMapping("/update-enable-analytics") @Hidden public ResponseEntity updateApiKey(@RequestBody Boolean enabled) throws IOException { - if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) { + if (applicationProperties.getSystem().getEnableAnalytics() != null) { return ResponseEntity.status(HttpStatus.ALREADY_REPORTED) .body( "Setting has already been set, To adjust please edit " + InstallationPathConfig.getSettingsPath()); } - GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false); - applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled)); + GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled); + applicationProperties.getSystem().setEnableAnalytics(enabled); return ResponseEntity.ok("Updated"); } } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 0b4783783..fdce513ab 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -284,10 +284,14 @@ public class ApplicationProperties { private boolean customHTMLFiles; private String tessdataDir; private Boolean enableAlphaFunctionality; - private String enableAnalytics; + private Boolean enableAnalytics; private Datasource datasource; private Boolean disableSanitize; private CustomPaths customPaths = new CustomPaths(); + + public boolean isAnalyticsEnabled() { + return this.getEnableAnalytics() != null && this.getEnableAnalytics(); + } } @Data diff --git a/src/main/java/stirling/software/SPDF/service/PostHogService.java b/src/main/java/stirling/software/SPDF/service/PostHogService.java index ab25080f7..8e7cd6abe 100644 --- a/src/main/java/stirling/software/SPDF/service/PostHogService.java +++ b/src/main/java/stirling/software/SPDF/service/PostHogService.java @@ -49,7 +49,7 @@ public class PostHogService { } private void captureSystemInfo() { - if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) { + if (!applicationProperties.getSystem().isAnalyticsEnabled()) { return; } try { @@ -60,7 +60,7 @@ public class PostHogService { } public void captureEvent(String eventName, Map properties) { - if (!Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics())) { + if (!applicationProperties.getSystem().isAnalyticsEnabled()) { return; } postHog.capture(uniqueId, eventName, properties); @@ -315,7 +315,7 @@ public class PostHogService { addIfNotEmpty( properties, "system_enableAnalytics", - applicationProperties.getSystem().getEnableAnalytics()); + applicationProperties.getSystem().isAnalyticsEnabled()); // Capture UI properties addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName()); diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 72e1034c3..1c259a4fb 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -9,15 +9,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; -import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Deque; import java.util.Enumeration; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.UUID; import org.springframework.web.multipart.MultipartFile; @@ -30,6 +25,7 @@ import io.github.pixee.security.Urls; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.InstallationPathConfig; +import stirling.software.SPDF.config.YamlHelper; @Slf4j 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 * *------------------------------------------------------------------------*/ - /** - * Actually performs the line-based update for the given path (e.g. "security.csrfDisabled") to - * 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) + public static void saveKeyToSettings(String key, Object newValue) throws IOException { + String[] keyArray = key.split("\\."); Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); - if (!Files.exists(settingsPath)) { - log.warn("Settings file not found at {}, creating a new empty file...", settingsPath); - Files.createDirectories(settingsPath.getParent()); - Files.createFile(settingsPath); - } - List 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 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 parseNestedYamlKeys(List lines) { - Map 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 pathStack = new ArrayDeque<>(); - Deque 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 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. - * - *

For example, oldLine might be: " csrfDisabled: false # set to 'true' to disable CSRF - * protection" newValue = "true" autoGenerated = false - * - *

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; + YamlHelper settingsYaml = new YamlHelper(settingsPath); + settingsYaml.updateValue(Arrays.asList(keyArray), newValue); + settingsYaml.saveOverride(settingsPath); } public static String generateMachineFingerprint() { diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 9ba176e88..2bcc0df5d 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -86,7 +86,7 @@ system: showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true' customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. - enableAnalytics: 'undefined' # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true + enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML) datasource: enableCustomDatabase: false # Enterprise users ONLY, set this property to 'true' if you would like to use your own custom database configuration @@ -111,7 +111,7 @@ ui: appName: '' # application's visible name homeDescription: '' # short description or tagline shown on the homepage appNameNavbar: '' # name displayed on the navigation bar - languages: [] # If empty, all languages ​​are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. + languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled. endpoints: toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) From d2bc281e42950437de0f8c0757451daba9370cfa Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:01:51 +0000 Subject: [PATCH 08/17] 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> --- src/main/resources/static/3rdPartyLicenses.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/resources/static/3rdPartyLicenses.json b/src/main/resources/static/3rdPartyLicenses.json index d035895b4..d9bcc4b47 100644 --- a/src/main/resources/static/3rdPartyLicenses.json +++ b/src/main/resources/static/3rdPartyLicenses.json @@ -1371,6 +1371,13 @@ "moduleLicense": "MIT License", "moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php" }, + { + "moduleName": "org.snakeyaml:snakeyaml-engine", + "moduleUrl": "https://bitbucket.org/snakeyaml/snakeyaml-engine", + "moduleVersion": "2.9", + "moduleLicense": "Apache License, Version 2.0", + "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" + }, { "moduleName": "org.springdoc:springdoc-openapi-starter-common", "moduleVersion": "2.2.0", From 222c18cdae38c65b051557e18973f3ad22aacef1 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:02:12 +0000 Subject: [PATCH 09/17] :globe_with_meridians: 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 --- README.md | 2 +- src/main/resources/messages_ga_IE.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 98857b41f..574076473 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Stirling-PDF currently supports 39 languages! | Hindi (हिंदी) (hi_IN) | ![98%](https://geps.dev/progress/98) | | Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) | | Indonesian (Bahasa Indonesia) (id_ID) | ![86%](https://geps.dev/progress/86) | -| Irish (Gaeilge) (ga_IE) | ![98%](https://geps.dev/progress/98) | +| Irish (Gaeilge) (ga_IE) | ![97%](https://geps.dev/progress/97) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Japanese (日本語) (ja_JP) | ![92%](https://geps.dev/progress/92) | | Korean (한국어) (ko_KR) | ![98%](https://geps.dev/progress/98) | diff --git a/src/main/resources/messages_ga_IE.properties b/src/main/resources/messages_ga_IE.properties index 154abd49b..aad7e77ca 100644 --- a/src/main/resources/messages_ga_IE.properties +++ b/src/main/resources/messages_ga_IE.properties @@ -572,8 +572,8 @@ login.invalid=Ainm úsáideora nó pasfhocal neamhbhailí. login.locked=Tá do chuntas glasáilte. login.signinTitle=Sínigh isteach le do thoil login.ssoSignIn=Logáil isteach trí Chlárú Aonair -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.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.oauth2RequestNotFound=Níor aimsíodh iarratas údaraithe login.oauth2InvalidUserInfoResponse=Freagra Neamhbhailí Faisnéise Úsáideora login.oauth2invalidRequest=Iarratas Neamhbhailí From 12b03be2bec79eb9f7d958ad0270f565aee5d8b9 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 22:09:30 +0000 Subject: [PATCH 10/17] :globe_with_meridians: 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> --- README.md | 2 +- src/main/resources/messages_ga_IE.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 574076473..98857b41f 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Stirling-PDF currently supports 39 languages! | Hindi (हिंदी) (hi_IN) | ![98%](https://geps.dev/progress/98) | | Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) | | Indonesian (Bahasa Indonesia) (id_ID) | ![86%](https://geps.dev/progress/86) | -| Irish (Gaeilge) (ga_IE) | ![97%](https://geps.dev/progress/97) | +| Irish (Gaeilge) (ga_IE) | ![98%](https://geps.dev/progress/98) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Japanese (日本語) (ja_JP) | ![92%](https://geps.dev/progress/92) | | Korean (한국어) (ko_KR) | ![98%](https://geps.dev/progress/98) | diff --git a/src/main/resources/messages_ga_IE.properties b/src/main/resources/messages_ga_IE.properties index aad7e77ca..52dfebae6 100644 --- a/src/main/resources/messages_ga_IE.properties +++ b/src/main/resources/messages_ga_IE.properties @@ -572,7 +572,7 @@ login.invalid=Ainm úsáideora nó pasfhocal neamhbhailí. login.locked=Tá do chuntas glasáilte. login.signinTitle=Sínigh isteach le do thoil 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.oauth2RequestNotFound=Níor aimsíodh iarratas údaraithe login.oauth2InvalidUserInfoResponse=Freagra Neamhbhailí Faisnéise Úsáideora From 77dec10f25c42db071efb8152b3ebd2ad885d7d0 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:46:11 +0000 Subject: [PATCH 11/17] 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. --- build.gradle | 2 +- .../resources/templates/fragments/navElements.html | 12 ------------ src/main/resources/templates/home-legacy.html | 6 ------ 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 9f0f4fd85..133525dfc 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { } group = "stirling.software" -version = "0.43.0" +version = "0.43.1" java { // 17 is lowest but we support and recommend 21 diff --git a/src/main/resources/templates/fragments/navElements.html b/src/main/resources/templates/fragments/navElements.html index 5b4e1b9e2..5b293a8a1 100644 --- a/src/main/resources/templates/fragments/navElements.html +++ b/src/main/resources/templates/fragments/navElements.html @@ -59,9 +59,6 @@

-
-
@@ -92,9 +89,6 @@
-
-
@@ -120,9 +114,6 @@
-
-
@@ -151,9 +142,6 @@
-
-
diff --git a/src/main/resources/templates/home-legacy.html b/src/main/resources/templates/home-legacy.html index 0d57f8df7..9e70d58f3 100644 --- a/src/main/resources/templates/home-legacy.html +++ b/src/main/resources/templates/home-legacy.html @@ -163,9 +163,6 @@
-
-
@@ -198,9 +195,6 @@
-
-
From 7a7338c6def2b84fb8eddd42afaaff29f3e38b19 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Wed, 26 Feb 2025 10:22:25 +0000 Subject: [PATCH 12/17] 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. --- .../software/SPDF/config/security/SecurityConfiguration.java | 2 +- .../SPDF/config/security/oauth2/OAuth2Configuration.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index 318ca1909..289071b03 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -285,7 +285,7 @@ public class SecurityConfiguration { }); } } else { - log.info("SAML 2 login is not enabled. Using default."); + log.debug("SAML 2 login is not enabled. Using default."); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } return http.build(); diff --git a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java index afe83bc7f..4fe03c486 100644 --- a/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java +++ b/src/main/java/stirling/software/SPDF/config/security/oauth2/OAuth2Configuration.java @@ -200,7 +200,7 @@ public class OAuth2Configuration { .scope(oidcProvider.getScopes()) .userNameAttributeName(oidcProvider.getUseAsUsername().getName()) .clientName(clientName) - .redirectUri(REDIRECT_URI_PATH + name) + .redirectUri(REDIRECT_URI_PATH + "oidc") .authorizationGrantType(AUTHORIZATION_CODE) .build()) : Optional.empty(); From c9c8378fe0d0c6a58eb318dcb2b644b79256f32a Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 26 Feb 2025 16:56:03 +0100 Subject: [PATCH 13/17] 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. --- .github/scripts/check_language_properties.py | 23 ++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/scripts/check_language_properties.py b/.github/scripts/check_language_properties.py index 70e63822c..10e6fb650 100644 --- a/.github/scripts/check_language_properties.py +++ b/.github/scripts/check_language_properties.py @@ -164,7 +164,7 @@ def update_missing_keys(reference_file, file_list, branch=""): if current_entry["type"] == "entry": if ref_entry_copy["type"] != "entry": 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"] updated_properties.append(ref_entry_copy) 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")) 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 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 - 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( - 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 ( basename_current_file == basename_reference_file or ( # only local windows command - not file_path.startswith( + not file_normpath.startswith( 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_") ) ) - or not file_path.endswith(".properties") + or not file_normpath.endswith(".properties") or not basename_current_file.startswith("messages_") ): continue @@ -292,13 +293,13 @@ def check_for_differences(reference_file, file_list, branch, actor): else: 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 output = "\n".join( [ f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`" for key, first, duplicate in find_duplicate_keys( - os.path.join(branch, file_path) + os.path.join(branch, file_normpath) ) ] ) From 366bec602d0833ec200da66c032639d8d19bf9c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:56:35 +0000 Subject: [PATCH 14/17] Bump ch.qos.logback:logback-core from 1.5.16 to 1.5.17 (#3068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ch.qos.logback:logback-core](https://github.com/qos-ch/logback) from 1.5.16 to 1.5.17.
Release notes

Sourced from ch.qos.logback:logback-core's releases.

Logback 1.5.17

2025-02-25 Release of logback version 1.5.17

• Fixed Jansi 2.4.0 color-coded output not working on Windows CMD.exe console when the default terminal application is set to "Windows Console Host". This problem was reported in issues/753 by Michael Lyubkin.

• 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.

• 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 "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

Commits
  • 1035872 prepare release 1.5.17
  • 2e6984d bump to slf4j version 2.0.17
  • 1009952 use a new LoggerContert instance when running LogbackListenerTest. This shoul...
  • a3bb4b0 Merge branch 'master' of github.com:qos-ch/logback
  • b507297 Fixed race condition occurring in case MDC class is initialized while org.slf...
  • f5b3bc5 add warning about the deprecation of SerializedModelConfigurator if activated
  • 5bc0998 Update README.md
  • 5610c96 correct relocation address
  • f3d100b update logback-access evaluator examples
  • 51e3903 fix issues/753 for the second time
  • Additional commits viewable in compare view

[![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) ---
Dependabot commands and options
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 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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 133525dfc..afa9f0840 100644 --- a/build.gradle +++ b/build.gradle @@ -294,7 +294,7 @@ configurations.all { dependencies { //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' From 8f7153b30a831fafa1163150be3abf47a34f6371 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:23:09 +0000 Subject: [PATCH 15/17] Bump ch.qos.logback:logback-classic from 1.5.16 to 1.5.17 (#3069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.16 to 1.5.17.
Release notes

Sourced from ch.qos.logback:logback-classic's releases.

Logback 1.5.17

2025-02-25 Release of logback version 1.5.17

• Fixed Jansi 2.4.0 color-coded output not working on Windows CMD.exe console when the default terminal application is set to "Windows Console Host". This problem was reported in issues/753 by Michael Lyubkin.

• 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.

• 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 "21" 2023-10-17 LTS build 21.0.1.+12-LTS-29 under Linux Debian 11.6.

Commits
  • 1035872 prepare release 1.5.17
  • 2e6984d bump to slf4j version 2.0.17
  • 1009952 use a new LoggerContert instance when running LogbackListenerTest. This shoul...
  • a3bb4b0 Merge branch 'master' of github.com:qos-ch/logback
  • b507297 Fixed race condition occurring in case MDC class is initialized while org.slf...
  • f5b3bc5 add warning about the deprecation of SerializedModelConfigurator if activated
  • 5bc0998 Update README.md
  • 5610c96 correct relocation address
  • f3d100b update logback-access evaluator examples
  • 51e3903 fix issues/753 for the second time
  • Additional commits viewable in compare view

[![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) ---
Dependabot commands and options
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 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)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index afa9f0840..c6b75caf1 100644 --- a/build.gradle +++ b/build.gradle @@ -295,7 +295,7 @@ dependencies { //tmp for security bumps 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 From 96655f7cacd47aad7c4d549554d21766d9f5a5bc Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:46:23 +0000 Subject: [PATCH 16/17] 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> --- src/main/resources/static/3rdPartyLicenses.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/static/3rdPartyLicenses.json b/src/main/resources/static/3rdPartyLicenses.json index d9bcc4b47..620eaeee6 100644 --- a/src/main/resources/static/3rdPartyLicenses.json +++ b/src/main/resources/static/3rdPartyLicenses.json @@ -3,14 +3,14 @@ { "moduleName": "ch.qos.logback:logback-classic", "moduleUrl": "http://www.qos.ch", - "moduleVersion": "1.5.16", + "moduleVersion": "1.5.17", "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, { "moduleName": "ch.qos.logback:logback-core", "moduleUrl": "http://www.qos.ch", - "moduleVersion": "1.5.16", + "moduleVersion": "1.5.17", "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, From 9152e64b9f091dc2fdfe4ad01b4e10bf74cbb853 Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 26 Feb 2025 20:25:35 +0100 Subject: [PATCH 17/17] 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. --- .../software/SPDF/utils/FileToPdf.java | 44 +++-------- .../software/SPDF/utils/ProcessExecutor.java | 10 ++- .../software/SPDF/utils/FileToPdfTest.java | 78 +++++++++++++++---- 3 files changed, 79 insertions(+), 53 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java index 6a0e263aa..dbc0915bb 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java +++ b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java @@ -169,7 +169,7 @@ public class FileToPdf { } } - // search for the main HTML file. + // Search for the main HTML file. try (Stream walk = Files.walk(tempDirectory)) { List htmlFiles = 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 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) { 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("..\\")) { entryName = entryName.replace("../", "").replace("..\\", ""); } + // Normalize all backslashes to forward slashes + entryName = entryName.replaceAll("\\\\", "/"); return entryName; } } diff --git a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java index a8d399697..e5b8fbb36 100644 --- a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java +++ b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java @@ -1,6 +1,10 @@ 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.util.ArrayList; import java.util.List; @@ -222,7 +226,7 @@ public class ProcessExecutor { boolean isQpdf = command != null && !command.isEmpty() && command.get(0).contains("qpdf"); - if (outputLines.size() > 0) { + if (!outputLines.isEmpty()) { String outputMessage = String.join("\n", outputLines); messages += outputMessage; if (!liveUpdates) { @@ -230,7 +234,7 @@ public class ProcessExecutor { } } - if (errorLines.size() > 0) { + if (!errorLines.isEmpty()) { String errorMessage = String.join("\n", errorLines); messages += errorMessage; if (!liveUpdates) { diff --git a/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java b/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java index f5cb2c802..8edb1b871 100644 --- a/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java +++ b/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java @@ -5,31 +5,79 @@ import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest; import java.io.IOException; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; public class FileToPdfTest { + /** + * Test the HTML to PDF conversion. + * This test expects an IOException when an empty HTML input is provided. + */ @Test public void testConvertHtmlToPdf() { HTMLToPdfRequest request = new HTMLToPdfRequest(); - byte[] fileBytes = new byte[0]; // Sample file bytes - String fileName = "test.html"; // Sample file name - boolean disableSanitize = false; // Sample boolean value + byte[] fileBytes = new byte[0]; // Sample file bytes (empty input) + String fileName = "test.html"; // Sample file name indicating an HTML file + boolean disableSanitize = false; // Flag to control sanitization - // Check if the method throws IOException - assertThrows(IOException.class, () -> { - FileToPdf.convertHtmlToPdf("/path/",request, fileBytes, fileName, disableSanitize); - }); + // Expect an IOException to be thrown due to empty input + Throwable thrown = + assertThrows( + IOException.class, + () -> + FileToPdf.convertHtmlToPdf( + "/path/", request, fileBytes, fileName, disableSanitize)); + assertNotNull(thrown); } + /** + * Test sanitizeZipFilename with null or empty input. + * It should return an empty string in these cases. + */ @Test - public void testConvertBookTypeToPdf() { - byte[] bytes = new byte[10]; // Sample bytes - String originalFilename = "test.epub"; // Sample original filename + public void testSanitizeZipFilename_NullOrEmpty() { + assertEquals("", FileToPdf.sanitizeZipFilename(null)); + assertEquals("", FileToPdf.sanitizeZipFilename(" ")); + } - // Check if the method throws IOException - assertThrows(IOException.class, () -> { - FileToPdf.convertBookTypeToPdf(bytes, originalFilename); - }); + /** + * Test sanitizeZipFilename to ensure it removes path traversal sequences. + * This includes removing both forward and backward slash sequences. + */ + @Test + public void testSanitizeZipFilename_RemovesTraversalSequences() { + String input = "../some/../path/..\\to\\file.txt"; + String expected = "some/path/to/file.txt"; + + // Print output for debugging purposes + System.out.println("sanitizeZipFilename " + FileToPdf.sanitizeZipFilename(input)); + System.out.flush(); + + // Expect that the method replaces backslashes with forward slashes + // and removes path traversal sequences + assertEquals(expected, FileToPdf.sanitizeZipFilename(input)); + } + + /** + * Test sanitizeZipFilename to ensure that it removes leading drive letters and slashes. + */ + @Test + public void testSanitizeZipFilename_RemovesLeadingDriveAndSlashes() { + String input = "C:\\folder\\file.txt"; + String expected = "folder/file.txt"; + assertEquals(expected, FileToPdf.sanitizeZipFilename(input)); + + input = "/folder/file.txt"; + expected = "folder/file.txt"; + assertEquals(expected, FileToPdf.sanitizeZipFilename(input)); + } + + /** + * Test sanitizeZipFilename to verify that safe filenames remain unchanged. + */ + @Test + public void testSanitizeZipFilename_NoChangeForSafeNames() { + String input = "folder/subfolder/file.txt"; + assertEquals(input, FileToPdf.sanitizeZipFilename(input)); } }