From fe9c5a7351f127b1c9de1832be71763841a9e327 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sat, 15 Jun 2024 10:05:31 +0200 Subject: [PATCH] Change: method write and read `settings.yml` #1441 (#1463) Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- build.gradle | 4 +- .../SPDF/config/ConfigInitializer.java | 165 +++++++++--------- .../config/security/InitialSecuritySetup.java | 35 +--- src/main/resources/settings.yml.template | 82 +++++---- 4 files changed, 142 insertions(+), 144 deletions(-) diff --git a/build.gradle b/build.gradle index 78901d8ee52..82ca30b5341 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ sourceCompatibility = '17' repositories { mavenCentral() + maven { url 'https://jitpack.io' } } licenseReport { @@ -74,7 +75,7 @@ spotless { java { target project.fileTree('src/main/java') - googleJavaFormat('1.19.1').aosp().reorderImports(false) + googleJavaFormat('1.22.0').aosp().reorderImports(false) importOrder('java', 'javax', 'org', 'com', 'net', 'io') toggleOffOn() @@ -93,6 +94,7 @@ dependencies { implementation("io.github.pixee:java-security-toolkit:1.1.3") implementation 'org.yaml:snakeyaml:2.2' + implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4' // Exclude Tomcat and include Jetty implementation('org.springframework.boot:spring-boot-starter-web:3.2.4') { diff --git a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java index f0d52e32148..c7af78f4d3a 100644 --- a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java +++ b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java @@ -4,17 +4,26 @@ import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; import java.util.List; +import org.simpleyaml.configuration.comments.CommentType; +import org.simpleyaml.configuration.file.YamlFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; public class ConfigInitializer implements ApplicationContextInitializer { + private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class); + @Override public void initialize(ConfigurableApplicationContext applicationContext) { try { @@ -44,95 +53,89 @@ public void ensureConfigExists() throws IOException, URISyntaxException { } } } else { - // Path templatePath = - // Paths.get( - // getClass() - // .getClassLoader() - // .getResource("settings.yml.template") - // .toURI()); - // Path userPath = Paths.get("configs", "settings.yml"); - // - // List templateLines = Files.readAllLines(templatePath); - // List userLines = - // Files.exists(userPath) ? Files.readAllLines(userPath) : new - // ArrayList<>(); - // - // List resultLines = new ArrayList<>(); - // int position = 0; - // for (String templateLine : templateLines) { - // // Check if the line is a comment - // if (templateLine.trim().startsWith("#")) { - // String entry = templateLine.trim().substring(1).trim(); - // if (!entry.isEmpty()) { - // // Check if this comment has been uncommented in userLines - // String key = entry.split(":")[0].trim(); - // addLine(resultLines, userLines, templateLine, key, position); - // } else { - // resultLines.add(templateLine); - // } - // } - // // Check if the line is a key-value pair - // else if (templateLine.contains(":")) { - // String key = templateLine.split(":")[0].trim(); - // addLine(resultLines, userLines, templateLine, key, position); - // } - // // Handle empty lines - // else if (templateLine.trim().length() == 0) { - // resultLines.add(""); - // } - // position++; - // } - // - // // Write the result to the user settings file - // Files.write(userPath, resultLines); - } - Path customSettingsPath = Paths.get("configs", "custom_settings.yml"); - if (!Files.exists(customSettingsPath)) { - Files.createFile(customSettingsPath); - } - } + // Define the path to the config settings file + Path settingsPath = Paths.get("configs", "settings.yml"); + // Load the template resource + URL settingsTemplateResource = + getClass().getClassLoader().getResource("settings.yml.template"); + if (settingsTemplateResource == null) { + throw new IOException("Resource not found: settings.yml.template"); + } + + // Create a temporary file to copy the resource content + Path tempTemplatePath = Files.createTempFile("settings.yml", ".template"); + + try (InputStream in = settingsTemplateResource.openStream()) { + Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); + } + + final YamlFile settingsTemplateFile = new YamlFile(tempTemplatePath.toFile()); + settingsTemplateFile.loadWithComments(); - // TODO check parent value instead of just indent lines for duplicate keys (like enabled etc) - private static void addLine( - List resultLines, - List userLines, - String templateLine, - String key, - int position) { - boolean added = false; - int templateIndentationLevel = getIndentationLevel(templateLine); - int pos = 0; - for (String settingsLine : userLines) { - if (settingsLine.trim().startsWith(key + ":") && position == pos) { - int settingsIndentationLevel = getIndentationLevel(settingsLine); - // Check if it is correct settingsLine and has the same parent as templateLine - if (settingsIndentationLevel == templateIndentationLevel) { - resultLines.add(settingsLine); - added = true; - break; + final YamlFile settingsFile = new YamlFile(settingsPath.toFile()); + settingsFile.loadWithComments(); + + // Load headers and comments + String header = settingsTemplateFile.getHeader(); + + // Create a new file for temporary settings + final YamlFile tempSettingFile = new YamlFile(settingsPath.toFile()); + tempSettingFile.createNewFile(true); + tempSettingFile.setHeader(header); + + // Get all keys from the template + List keys = + Arrays.asList(settingsTemplateFile.getKeys(true).toArray(new String[0])); + + for (String key : keys) { + if (!key.contains(".")) { + // Add blank lines and comments to specific sections + tempSettingFile + .path(key) + .comment(settingsTemplateFile.getComment(key)) + .blankLine(); + continue; } + // Copy settings from the template to the settings.yml file + changeConfigItemFromCommentToKeyValue( + settingsTemplateFile, settingsFile, tempSettingFile, key); } - pos++; + + // Save the settings.yml file + tempSettingFile.save(); } - if (!added) { - resultLines.add(templateLine); + + // Create custom settings file if it doesn't exist + Path customSettingsPath = Paths.get("configs", "custom_settings.yml"); + if (!Files.exists(customSettingsPath)) { + Files.createFile(customSettingsPath); } } - private static int getIndentationLevel(String line) { - int indentationLevel = 0; - String trimmedLine = line.trim(); - if (trimmedLine.startsWith("#")) { - line = trimmedLine.substring(1); - } - for (char c : line.toCharArray()) { - if (c == ' ') { - indentationLevel++; - } else { - break; - } + private void changeConfigItemFromCommentToKeyValue( + final YamlFile settingsTemplateFile, + final YamlFile settingsFile, + final YamlFile tempSettingFile, + String path) { + if (settingsFile.get(path) == null && settingsTemplateFile.get(path) != null) { + // If the key is only in the template, add it to the temporary settings with comments + tempSettingFile + .path(path) + .set(settingsTemplateFile.get(path)) + .comment(settingsTemplateFile.getComment(path, CommentType.BLOCK)) + .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE)); + } else if (settingsFile.get(path) != null && settingsTemplateFile.get(path) != null) { + // If the key is in both, update the temporary settings with the main settings' value + // and comments + tempSettingFile + .path(path) + .set(settingsFile.get(path)) + .comment(settingsTemplateFile.getComment(path, CommentType.BLOCK)) + .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE)); + } else { + // Log if the key is not found in both YAML files + logger.info("Key not found in both YAML files: " + path); } - return indentationLevel; } } 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 452de53e4a1..e696c6bc19a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -1,12 +1,11 @@ package stirling.software.SPDF.config.security; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.UUID; +import org.simpleyaml.configuration.file.YamlFile; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -87,32 +86,16 @@ private void initializeInternalApiUser() { private void saveKeyToConfig(String key) throws IOException { Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml - List lines = Files.readAllLines(path); - boolean keyFound = false; - - // Search for the existing key to replace it or place to add it - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).startsWith("AutomaticallyGenerated:")) { - keyFound = true; - if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { - lines.set(i + 1, " key: " + key); - break; - } else { - lines.add(i + 1, " key: " + key); - break; - } - } - } - // If the section doesn't exist, append it - if (!keyFound) { - lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); - lines.add("AutomaticallyGenerated:"); - lines.add(" key: " + key); - } + final YamlFile settingsYml = new YamlFile(path.toFile()); + + settingsYml.loadWithComments(); - // Write back to the file - Files.write(path, lines); + settingsYml + .path("AutomaticallyGenerated.key") + .set(key) + .comment("# Automatically Generated Settings (Do Not Edit Directly)"); + settingsYml.save(); } private boolean isValidUUID(String uuid) { diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index b15ca94f6a7..86d88ea1cf5 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -1,44 +1,54 @@ -# Welcome to settings file -# Remove comment marker # if on start of line to enable the configuration -# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME +############################################################################################################# +# Welcome to settings file from # +# ____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____ # +# / ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___| # +# \___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_ # +# ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| # +# |____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| # +# # +# Do not comment out any entry, it will be removed on next startup # +# If you want to override with environment parameter follow parameter naming SECURITY_INITIALLOGIN_USERNAME # +############################################################################################################# + security: enableLogin: false # set to 'true' to enable login csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production) loginAttemptCount: 5 # lock user account after 5 tries loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts -# initialLogin: -# username: "admin" # Initial username for the first login -# password: "stirling" # Initial password for the first login -# oauth2: -# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work) -# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point -# clientId: "" # Client ID from your provider -# clientSecret: "" # Client Secret from your provider -# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users -# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username -# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions -# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' -# client: -# google: -# clientId: "" # Client ID for Google OAuth2 -# clientSecret: "" # Client Secret for Google OAuth2 -# scopes: "https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile" # Scopes for Google OAuth2 -# useAsUsername: "email" # Field to use as the username for Google OAuth2 -# github: -# clientId: "" # Client ID for GitHub OAuth2 -# clientSecret: "" # Client Secret for GitHub OAuth2 -# scopes: "read:user" # Scope for GitHub OAuth2 -# useAsUsername: "login" # Field to use as the username for GitHub OAuth2 -# keycloak: -# issuer: "http://192.168.0.123:8888/realms/stirling-pdf" # URL of the Keycloak realm's OpenID Connect Discovery endpoint -# clientId: "stirling-pdf" # Client ID for Keycloak OAuth2 -# clientSecret: "" # Client Secret for Keycloak OAuth2 -# scopes: "openid, profile, email" # Scopes for Keycloak OAuth2 -# useAsUsername: "email" # Field to use as the username for Keycloak OAuth2 + loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2) + initialLogin: + username: '' # Initial username for the first login + password: '' # Initial password for the first login + oauth2: + enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work) + client: + keycloak: + issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint + clientId: '' # Client ID for Keycloak OAuth2 + clientSecret: '' # Client Secret for Keycloak OAuth2 + scopes: openid, profile, email # Scopes for Keycloak OAuth2 + useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2 + google: + clientId: '' # Client ID for Google OAuth2 + clientSecret: '' # Client Secret for Google OAuth2 + scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2 + useAsUsername: email # Field to use as the username for Google OAuth2 + github: + clientId: '' # Client ID for GitHub OAuth2 + clientSecret: '' # Client Secret for GitHub OAuth2 + scopes: read:user # Scope for GitHub OAuth2 + useAsUsername: login # Field to use as the username for GitHub OAuth2 + issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point + clientId: '' # Client ID from your provider + clientSecret: '' # Client Secret from your provider + autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users + useAsUsername: email # Default is 'email'; custom fields can be used as the username + scopes: openid, profile, email # Specify the scopes for which the application will request permissions + provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak' system: - defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) + defaultLocale: en-US # Set the default language (e.g. 'de-DE', 'fr-FR', etc) googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes) showUpdate: false # see when a new update is available @@ -46,9 +56,9 @@ system: customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files ui: - appName: null # Application's visible name - homeDescription: null # Short description or tagline shown on homepage. - appNameNavbar: null # Name displayed on the navigation bar + appName: '' # Application's visible name + homeDescription: '' # Short description or tagline shown on homepage. + appNameNavbar: '' # Name displayed on the navigation bar endpoints: toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])