Compare commits
21 Commits
pre-commit
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c7b83ee87 | ||
|
|
9152e64b9f | ||
|
|
96655f7cac | ||
|
|
8f7153b30a | ||
|
|
366bec602d | ||
|
|
c9c8378fe0 | ||
|
|
7a7338c6de | ||
|
|
77dec10f25 | ||
|
|
12b03be2be | ||
|
|
222c18cdae | ||
|
|
d2bc281e42 | ||
|
|
ac10c9fa43 | ||
|
|
4fabc07a44 | ||
|
|
2ab951e080 | ||
|
|
a1f7bb3e4a | ||
|
|
f64d7d42d9 | ||
|
|
300011f9b6 | ||
|
|
e328833f02 | ||
|
|
4c701b2e69 | ||
|
|
16295c7bb9 | ||
|
|
69da443096 |
23
.github/scripts/check_language_properties.py
vendored
23
.github/scripts/check_language_properties.py
vendored
@@ -164,7 +164,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
if current_entry["type"] == "entry":
|
||||
if 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)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
2
.github/workflows/licenses-update.yml
vendored
2
.github/workflows/licenses-update.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/pre_commit.yml
vendored
2
.github/workflows/pre_commit.yml
vendored
@@ -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"
|
||||
|
||||
2
.github/workflows/sync_files.yml
vendored
2
.github/workflows/sync_files.yml
vendored
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ ext {
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.42.0"
|
||||
version = "0.43.1"
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
@@ -294,8 +294,8 @@ configurations.all {
|
||||
dependencies {
|
||||
|
||||
//tmp for security bumps
|
||||
implementation 'ch.qos.logback:logback-core:1.5.16'
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.16'
|
||||
implementation 'ch.qos.logback:logback-core:1.5.17'
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.17'
|
||||
|
||||
|
||||
// Exclude vulnerable BouncyCastle version used in tableau
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import stirling.software.SPDF.utils.GeneralUtils;
|
||||
@Service
|
||||
@Slf4j
|
||||
public class KeygenLicenseVerifier {
|
||||
// todo: place in config files?
|
||||
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
||||
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
@@ -68,7 +69,7 @@ public class KeygenLicenseVerifier {
|
||||
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("Error verifying license: " + e.getMessage());
|
||||
log.error("Error verifying license: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -95,10 +96,9 @@ public class KeygenLicenseVerifier {
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
log.debug(" validateLicenseResponse body: " + response.body());
|
||||
log.debug("ValidateLicenseResponse body: {}", response.body());
|
||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||
if (response.statusCode() == 200) {
|
||||
|
||||
JsonNode metaNode = jsonResponse.path("meta");
|
||||
boolean isValid = metaNode.path("valid").asBoolean();
|
||||
|
||||
@@ -120,7 +120,7 @@ public class KeygenLicenseVerifier {
|
||||
log.info(applicationProperties.toString());
|
||||
|
||||
} else {
|
||||
log.error("Error validating license. Status code: " + response.statusCode());
|
||||
log.error("Error validating license. Status code: {}", response.statusCode());
|
||||
}
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package stirling.software.SPDF;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -29,6 +28,7 @@ import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.SPDF.config.ConfigInitializer;
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.utils.UrlUtils;
|
||||
|
||||
@Slf4j
|
||||
@EnableScheduling
|
||||
@@ -64,6 +64,12 @@ public class SPDFApplication {
|
||||
app.setHeadless(false);
|
||||
props.put("java.awt.headless", "false");
|
||||
props.put("spring.main.web-application-type", "servlet");
|
||||
|
||||
int desiredPort = 8080;
|
||||
String port = UrlUtils.findAvailablePort(desiredPort);
|
||||
props.put("server.port", port);
|
||||
System.setProperty("server.port", port);
|
||||
log.info("Desktop UI mode: Using port {}", port);
|
||||
}
|
||||
|
||||
app.setAdditionalProfiles(getActiveProfile(args));
|
||||
@@ -77,18 +83,18 @@ public class SPDFApplication {
|
||||
Map<String, String> propertyFiles = new HashMap<>();
|
||||
|
||||
// External config files
|
||||
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
|
||||
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
|
||||
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath());
|
||||
log.info("Settings file: {}", settingsPath.toString());
|
||||
if (Files.exists(settingsPath)) {
|
||||
propertyFiles.put(
|
||||
"spring.config.additional-location",
|
||||
"file:" + InstallationPathConfig.getSettingsPath());
|
||||
"spring.config.additional-location", "file:" + settingsPath.toString());
|
||||
} else {
|
||||
log.warn(
|
||||
"External configuration file '{}' does not exist.",
|
||||
InstallationPathConfig.getSettingsPath());
|
||||
log.warn("External configuration file '{}' does not exist.", settingsPath.toString());
|
||||
}
|
||||
|
||||
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
|
||||
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath());
|
||||
log.info("Custom settings file: {}", customSettingsPath.toString());
|
||||
if (Files.exists(customSettingsPath)) {
|
||||
String existingLocation =
|
||||
propertyFiles.getOrDefault("spring.config.additional-location", "");
|
||||
if (!existingLocation.isEmpty()) {
|
||||
@@ -96,11 +102,11 @@ public class SPDFApplication {
|
||||
}
|
||||
propertyFiles.put(
|
||||
"spring.config.additional-location",
|
||||
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
|
||||
existingLocation + "file:" + customSettingsPath.toString());
|
||||
} else {
|
||||
log.warn(
|
||||
"Custom configuration file '{}' does not exist.",
|
||||
InstallationPathConfig.getCustomSettingsPath());
|
||||
customSettingsPath.toString());
|
||||
}
|
||||
Properties finalProps = new Properties();
|
||||
|
||||
@@ -122,7 +128,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());
|
||||
}
|
||||
|
||||
@@ -151,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());
|
||||
}
|
||||
}
|
||||
@@ -160,7 +166,17 @@ public class SPDFApplication {
|
||||
}
|
||||
|
||||
@Value("${server.port:8080}")
|
||||
public void setServerPortStatic(String port) {
|
||||
public void setServerPort(String port) {
|
||||
if ("auto".equalsIgnoreCase(port)) {
|
||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||
SPDFApplication.serverPortStatic =
|
||||
"0"; // This will let Spring Boot assign an available port
|
||||
} else {
|
||||
SPDFApplication.serverPortStatic = port;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setServerPortStatic(String port) {
|
||||
if ("auto".equalsIgnoreCase(port)) {
|
||||
// Use Spring Boot's automatic port assignment (server.port=0)
|
||||
SPDFApplication.serverPortStatic =
|
||||
@@ -197,36 +213,11 @@ public class SPDFApplication {
|
||||
return new String[] {"default"};
|
||||
}
|
||||
|
||||
private static boolean isPortAvailable(int port) {
|
||||
try (ServerSocket socket = new ServerSocket(port)) {
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
|
||||
private static String findAvailablePort(int startPort) {
|
||||
int port = startPort;
|
||||
while (!isPortAvailable(port)) {
|
||||
port++;
|
||||
}
|
||||
return String.valueOf(port);
|
||||
}
|
||||
|
||||
public static String getStaticBaseUrl() {
|
||||
return baseUrlStatic;
|
||||
}
|
||||
|
||||
public String getNonStaticBaseUrl() {
|
||||
return baseUrlStatic;
|
||||
}
|
||||
|
||||
public static String getStaticPort() {
|
||||
return serverPortStatic;
|
||||
}
|
||||
|
||||
public String getNonStaticPort() {
|
||||
return serverPortStatic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,7 @@ public class AppConfig {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "system.customHTMLFiles",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
@@ -99,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")
|
||||
@@ -129,8 +126,8 @@ public class AppConfig {
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
|
||||
@Bean(name = "activSecurity")
|
||||
public boolean missingActivSecurity() {
|
||||
@Bean(name = "activeSecurity")
|
||||
public boolean missingActiveSecurity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -173,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")
|
||||
|
||||
@@ -20,7 +20,7 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
"endpoints",
|
||||
"logout",
|
||||
"error",
|
||||
"erroroauth",
|
||||
"errorOAuth",
|
||||
"file",
|
||||
"messageType",
|
||||
"infoMessage");
|
||||
|
||||
@@ -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<String> templateLines = Files.readAllLines(tempTemplatePath);
|
||||
List<String> 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<String> 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<String> mergeYamlLinesWithTemplate(
|
||||
List<String> templateLines, List<String> mainLines) {
|
||||
|
||||
// 1) Parse template lines into an ordered map: path -> Block
|
||||
LinkedHashMap<String, Block> templateBlocks = parseYamlBlocks(templateLines);
|
||||
|
||||
// 2) Parse main lines into a map: path -> Block
|
||||
LinkedHashMap<String, Block> mainBlocks = parseYamlBlocks(mainLines);
|
||||
|
||||
// 3) Build the final list by iterating template blocks in order
|
||||
List<String> merged = new ArrayList<>();
|
||||
for (Map.Entry<String, Block> entry : templateBlocks.entrySet()) {
|
||||
String path = entry.getKey();
|
||||
Block templateBlock = entry.getValue();
|
||||
|
||||
if (mainBlocks.containsKey(path)) {
|
||||
// If main has the same block, prefer main's lines
|
||||
merged.addAll(mainBlocks.get(path).lines);
|
||||
} else {
|
||||
// Otherwise, add the template block
|
||||
merged.addAll(templateBlock.lines);
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of lines into a map of "path -> Block" where "Block" is all lines that belong to
|
||||
* that key (including subsequent indented lines). Very naive approach that may not work with
|
||||
* advanced YAML.
|
||||
*/
|
||||
private LinkedHashMap<String, Block> parseYamlBlocks(List<String> lines) {
|
||||
LinkedHashMap<String, Block> blocks = new LinkedHashMap<>();
|
||||
|
||||
Block currentBlock = null;
|
||||
String currentPath = null;
|
||||
|
||||
for (String line : lines) {
|
||||
if (isLikelyKeyLine(line)) {
|
||||
// Found a new "key: ..." line
|
||||
if (currentBlock != null && currentPath != null) {
|
||||
blocks.put(currentPath, currentBlock);
|
||||
}
|
||||
currentBlock = new Block();
|
||||
currentBlock.lines.add(line);
|
||||
currentPath = computePathForLine(line);
|
||||
} else {
|
||||
// Continuation of current block (comments, blank lines, sub-lines)
|
||||
if (currentBlock == null) {
|
||||
// If file starts with comments/blank lines, treat as "header block" with path
|
||||
// ""
|
||||
currentBlock = new Block();
|
||||
currentPath = "";
|
||||
}
|
||||
currentBlock.lines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentBlock != null && currentPath != null) {
|
||||
blocks.put(currentPath, currentBlock);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the line is likely "key:" or "key: value", ignoring comments/blank. Skips lines
|
||||
* starting with "-" or "#".
|
||||
*/
|
||||
private boolean isLikelyKeyLine(String line) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||
return false;
|
||||
}
|
||||
int colonIdx = trimmed.indexOf(':');
|
||||
return (colonIdx > 0); // someKey:
|
||||
}
|
||||
|
||||
// For a line like "security: ", returns "security" or "security.enableLogin"
|
||||
// by looking at indentation. Very naive.
|
||||
private static final Deque<String> pathStack = new ArrayDeque<>();
|
||||
private static int currentIndentLevel = 0;
|
||||
|
||||
private String computePathForLine(String line) {
|
||||
// count leading spaces
|
||||
int leadingSpaces = 0;
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == ' ') leadingSpaces++;
|
||||
else break;
|
||||
}
|
||||
// assume 2 spaces = 1 indent
|
||||
int indentLevel = leadingSpaces / 2;
|
||||
|
||||
String trimmed = line.trim();
|
||||
int colonIdx = trimmed.indexOf(':');
|
||||
String keyName = trimmed.substring(0, colonIdx).trim();
|
||||
|
||||
// pop stack until we match the new indent level
|
||||
while (currentIndentLevel >= indentLevel && !pathStack.isEmpty()) {
|
||||
pathStack.pop();
|
||||
currentIndentLevel--;
|
||||
}
|
||||
|
||||
// push the new key
|
||||
pathStack.push(keyName);
|
||||
currentIndentLevel = indentLevel;
|
||||
|
||||
// build path by reversing the stack
|
||||
String[] arr = pathStack.toArray(new String[0]);
|
||||
List<String> reversed = Arrays.asList(arr);
|
||||
Collections.reverse(reversed);
|
||||
return String.join(".", reversed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple holder for the lines that comprise a "block" (i.e. a key and its subsequent lines).
|
||||
*/
|
||||
private static class Block {
|
||||
List<String> lines = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,54 +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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
479
src/main/java/stirling/software/SPDF/config/YamlHelper.java
Normal file
479
src/main/java/stirling/software/SPDF/config/YamlHelper.java
Normal file
@@ -0,0 +1,479 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.snakeyaml.engine.v2.api.Dump;
|
||||
import org.snakeyaml.engine.v2.api.DumpSettings;
|
||||
import org.snakeyaml.engine.v2.api.LoadSettings;
|
||||
import org.snakeyaml.engine.v2.api.StreamDataWriter;
|
||||
import org.snakeyaml.engine.v2.common.FlowStyle;
|
||||
import org.snakeyaml.engine.v2.common.ScalarStyle;
|
||||
import org.snakeyaml.engine.v2.composer.Composer;
|
||||
import org.snakeyaml.engine.v2.nodes.MappingNode;
|
||||
import org.snakeyaml.engine.v2.nodes.Node;
|
||||
import org.snakeyaml.engine.v2.nodes.NodeTuple;
|
||||
import org.snakeyaml.engine.v2.nodes.ScalarNode;
|
||||
import org.snakeyaml.engine.v2.nodes.SequenceNode;
|
||||
import org.snakeyaml.engine.v2.nodes.Tag;
|
||||
import org.snakeyaml.engine.v2.parser.ParserImpl;
|
||||
import org.snakeyaml.engine.v2.scanner.StreamReader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class YamlHelper {
|
||||
|
||||
// YAML dump settings with comment support and block flow style
|
||||
private static final DumpSettings DUMP_SETTINGS =
|
||||
DumpSettings.builder()
|
||||
.setDumpComments(true)
|
||||
.setWidth(Integer.MAX_VALUE)
|
||||
.setDefaultFlowStyle(FlowStyle.BLOCK)
|
||||
.build();
|
||||
|
||||
private final String yamlContent; // Stores the entire YAML content as a string
|
||||
|
||||
private LoadSettings loadSettings =
|
||||
LoadSettings.builder()
|
||||
.setUseMarks(true)
|
||||
.setMaxAliasesForCollections(Integer.MAX_VALUE)
|
||||
.setAllowRecursiveKeys(true)
|
||||
.setParseComments(true)
|
||||
.build();
|
||||
|
||||
private Path originalFilePath;
|
||||
private Node updatedRootNode;
|
||||
|
||||
// Constructor with custom LoadSettings and YAML string
|
||||
public YamlHelper(LoadSettings loadSettings, String yamlContent) {
|
||||
this.loadSettings = loadSettings;
|
||||
this.yamlContent = yamlContent;
|
||||
}
|
||||
|
||||
// Constructor that reads YAML from a file path
|
||||
public YamlHelper(Path originalFilePath) throws IOException {
|
||||
this.yamlContent = Files.readString(originalFilePath);
|
||||
this.originalFilePath = originalFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates values in the target YAML based on values from the source YAML. It ensures that only
|
||||
* existing keys in the target YAML are updated.
|
||||
*
|
||||
* @return true if at least one key was updated, false otherwise.
|
||||
*/
|
||||
public boolean updateValuesFromYaml(YamlHelper sourceYaml, YamlHelper targetYaml) {
|
||||
boolean updated = false;
|
||||
Set<String> sourceKeys = sourceYaml.getAllKeys();
|
||||
Set<String> targetKeys = targetYaml.getAllKeys();
|
||||
|
||||
for (String key : sourceKeys) {
|
||||
String[] keyArray = key.split("\\.");
|
||||
|
||||
Object newValue = sourceYaml.getValueByExactKeyPath(keyArray);
|
||||
Object currentValue = targetYaml.getValueByExactKeyPath(keyArray);
|
||||
if (newValue != null
|
||||
&& (!newValue.equals(currentValue) || !sourceKeys.equals(targetKeys))) {
|
||||
boolean updatedKey = targetYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||
if (updatedKey) updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a value in the YAML structure.
|
||||
*
|
||||
* @param keys The hierarchical keys leading to the value.
|
||||
* @param newValue The new value to set.
|
||||
* @return true if the value was updated, false otherwise.
|
||||
*/
|
||||
public boolean updateValue(List<String> keys, Object newValue) {
|
||||
return updateValue(getRootNode(), keys, newValue);
|
||||
}
|
||||
|
||||
private boolean updateValue(Node node, List<String> keys, Object newValue) {
|
||||
if (!(node instanceof MappingNode mappingNode)) return false;
|
||||
|
||||
List<NodeTuple> updatedTuples = new ArrayList<>();
|
||||
boolean updated = false;
|
||||
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
ScalarNode keyNode = (tuple.getKeyNode() instanceof ScalarNode sk) ? sk : null;
|
||||
if (keyNode == null || !keyNode.getValue().equals(keys.get(0))) {
|
||||
updatedTuples.add(tuple);
|
||||
continue;
|
||||
}
|
||||
|
||||
Node valueNode = tuple.getValueNode();
|
||||
|
||||
if (keys.size() == 1) {
|
||||
Tag tag = valueNode.getTag();
|
||||
Node newValueNode = null;
|
||||
|
||||
if (isAnyInteger(newValue)) {
|
||||
newValueNode =
|
||||
new ScalarNode(Tag.INT, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else if (isFloat(newValue)) {
|
||||
Object floatValue = Float.valueOf(String.valueOf(newValue));
|
||||
newValueNode =
|
||||
new ScalarNode(
|
||||
Tag.FLOAT, String.valueOf(floatValue), ScalarStyle.PLAIN);
|
||||
} else if ("true".equals(newValue) || "false".equals(newValue)) {
|
||||
newValueNode =
|
||||
new ScalarNode(Tag.BOOL, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else if (newValue instanceof List<?> list) {
|
||||
List<Node> sequenceNodes = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
Object obj = String.valueOf(item);
|
||||
if (isAnyInteger(item)) {
|
||||
tag = Tag.INT;
|
||||
} else if (isFloat(item)) {
|
||||
obj = Float.valueOf(String.valueOf(item));
|
||||
tag = Tag.FLOAT;
|
||||
} else if ("true".equals(item) || "false".equals(item)) {
|
||||
tag = Tag.BOOL;
|
||||
} else if (item == null || "null".equals(item)) {
|
||||
tag = Tag.NULL;
|
||||
} else {
|
||||
tag = Tag.STR;
|
||||
}
|
||||
sequenceNodes.add(
|
||||
new ScalarNode(tag, String.valueOf(obj), ScalarStyle.PLAIN));
|
||||
}
|
||||
newValueNode = new SequenceNode(Tag.SEQ, sequenceNodes, FlowStyle.FLOW);
|
||||
} else if (tag == Tag.NULL) {
|
||||
if ("true".equals(newValue)
|
||||
|| "false".equals(newValue)
|
||||
|| newValue instanceof Boolean) {
|
||||
tag = Tag.BOOL;
|
||||
}
|
||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
} else {
|
||||
newValueNode = new ScalarNode(tag, String.valueOf(newValue), ScalarStyle.PLAIN);
|
||||
}
|
||||
copyComments(valueNode, newValueNode);
|
||||
|
||||
updatedTuples.add(new NodeTuple(keyNode, newValueNode));
|
||||
updated = true;
|
||||
} else if (valueNode instanceof MappingNode) {
|
||||
updated = updateValue(valueNode, keys.subList(1, keys.size()), newValue);
|
||||
updatedTuples.add(tuple);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
mappingNode.getValue().clear();
|
||||
mappingNode.getValue().addAll(updatedTuples);
|
||||
}
|
||||
setNewNode(node);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a value based on an exact key path.
|
||||
*
|
||||
* @param keys The key hierarchy leading to the value.
|
||||
* @return The value if found, otherwise null.
|
||||
*/
|
||||
public Object getValueByExactKeyPath(String... keys) {
|
||||
return getValueByExactKeyPath(getRootNode(), new ArrayDeque<>(List.of(keys)));
|
||||
}
|
||||
|
||||
private Object getValueByExactKeyPath(Node node, Deque<String> keyQueue) {
|
||||
if (!(node instanceof MappingNode mappingNode)) return null;
|
||||
|
||||
String currentKey = keyQueue.poll();
|
||||
if (currentKey == null) return null;
|
||||
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode
|
||||
&& keyNode.getValue().equals(currentKey)) {
|
||||
if (keyQueue.isEmpty()) {
|
||||
Node valueNode = tuple.getValueNode();
|
||||
|
||||
if (valueNode instanceof ScalarNode scalarValueNode) {
|
||||
return scalarValueNode.getValue();
|
||||
} else if (valueNode instanceof MappingNode subMapping) {
|
||||
return getValueByExactKeyPath(subMapping, keyQueue);
|
||||
} else if (valueNode instanceof SequenceNode sequenceNode) {
|
||||
List<Object> valuesList = new ArrayList<>();
|
||||
for (Node o : sequenceNode.getValue()) {
|
||||
if (o instanceof ScalarNode scalarValue) {
|
||||
valuesList.add(scalarValue.getValue());
|
||||
}
|
||||
}
|
||||
return valuesList;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return getValueByExactKeyPath(tuple.getValueNode(), keyQueue);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Set<String> cachedKeys;
|
||||
|
||||
/**
|
||||
* Retrieves the set of all keys present in the YAML structure. Keys are returned as
|
||||
* dot-separated paths for nested keys.
|
||||
*
|
||||
* @return A set containing all keys in dot notation.
|
||||
*/
|
||||
public Set<String> getAllKeys() {
|
||||
if (cachedKeys == null) {
|
||||
cachedKeys = getAllKeys(getRootNode());
|
||||
}
|
||||
return cachedKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all keys from the YAML node recursively.
|
||||
*
|
||||
* @param node The current YAML node.
|
||||
* @param currentPath The accumulated path of keys.
|
||||
* @param allKeys The set storing all collected keys.
|
||||
*/
|
||||
private Set<String> getAllKeys(Node node) {
|
||||
Set<String> allKeys = new LinkedHashSet<>();
|
||||
collectKeys(node, "", allKeys);
|
||||
return allKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses the YAML structure to collect all keys.
|
||||
*
|
||||
* @param node The current node in the YAML structure.
|
||||
* @param currentPath The accumulated key path.
|
||||
* @param allKeys The set storing collected keys.
|
||||
*/
|
||||
private void collectKeys(Node node, String currentPath, Set<String> allKeys) {
|
||||
if (node instanceof MappingNode mappingNode) {
|
||||
for (NodeTuple tuple : mappingNode.getValue()) {
|
||||
if (tuple.getKeyNode() instanceof ScalarNode keyNode) {
|
||||
String newPath =
|
||||
currentPath.isEmpty()
|
||||
? keyNode.getValue()
|
||||
: currentPath + "." + keyNode.getValue();
|
||||
allKeys.add(newPath);
|
||||
collectKeys(tuple.getValueNode(), newPath, allKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the root node of the YAML document. If a new node was previously set, it is
|
||||
* returned instead.
|
||||
*
|
||||
* @return The root node of the YAML structure.
|
||||
*/
|
||||
private Node getRootNode() {
|
||||
if (this.updatedRootNode != null) {
|
||||
return this.updatedRootNode;
|
||||
}
|
||||
Composer composer = new Composer(loadSettings, getParserImpl());
|
||||
Optional<Node> rootNodeOpt = composer.getSingleNode();
|
||||
if (rootNodeOpt.isPresent()) {
|
||||
return rootNodeOpt.get();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a new root node, allowing modifications to be tracked.
|
||||
*
|
||||
* @param newRootNode The modified root node.
|
||||
*/
|
||||
public void setNewNode(Node newRootNode) {
|
||||
this.updatedRootNode = newRootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current root node (either the original or the updated one).
|
||||
*
|
||||
* @return The root node.
|
||||
*/
|
||||
public Node getUpdatedRootNode() {
|
||||
if (this.updatedRootNode == null) {
|
||||
this.updatedRootNode = getRootNode();
|
||||
}
|
||||
return this.updatedRootNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the YAML parser.
|
||||
*
|
||||
* @return The configured parser.
|
||||
*/
|
||||
private ParserImpl getParserImpl() {
|
||||
return new ParserImpl(loadSettings, getStreamReader());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a stream reader for the YAML content.
|
||||
*
|
||||
* @return The configured stream reader.
|
||||
*/
|
||||
private StreamReader getStreamReader() {
|
||||
return new StreamReader(loadSettings, yamlContent);
|
||||
}
|
||||
|
||||
public MappingNode save(Path saveFilePath) throws IOException {
|
||||
if (!saveFilePath.equals(originalFilePath)) {
|
||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||
}
|
||||
return (MappingNode) getUpdatedRootNode();
|
||||
}
|
||||
|
||||
public void saveOverride(Path saveFilePath) throws IOException {
|
||||
Files.writeString(saveFilePath, convertNodeToYaml(getUpdatedRootNode()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a YAML node back to a YAML-formatted string.
|
||||
*
|
||||
* @param rootNode The root node to be converted.
|
||||
* @return A YAML-formatted string.
|
||||
*/
|
||||
public String convertNodeToYaml(Node rootNode) {
|
||||
StringWriter writer = new StringWriter();
|
||||
StreamDataWriter streamDataWriter =
|
||||
new StreamDataWriter() {
|
||||
@Override
|
||||
public void write(String str) {
|
||||
writer.write(str);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String str, int off, int len) {
|
||||
writer.write(str, off, len);
|
||||
}
|
||||
};
|
||||
|
||||
new Dump(DUMP_SETTINGS).dumpNode(rootNode, streamDataWriter);
|
||||
return writer.toString();
|
||||
}
|
||||
|
||||
private static boolean isParsable(String value, Function<String, ?> parser) {
|
||||
try {
|
||||
parser.apply(value);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is an integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents an integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isInteger(Object object) {
|
||||
if (object instanceof Integer
|
||||
|| object instanceof Short
|
||||
|| object instanceof Byte
|
||||
|| object instanceof Long) {
|
||||
return true;
|
||||
}
|
||||
if (object instanceof String str) {
|
||||
return isParsable(str, Integer::parseInt);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a floating-point number.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a float, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isFloat(Object object) {
|
||||
return (object instanceof Float || object instanceof Double)
|
||||
|| (object instanceof String str && isParsable(str, Float::parseFloat));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a short integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a short integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isShort(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Short::parseShort));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a byte.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a byte, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isByte(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Byte::parseByte));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given object is a long integer.
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents a long integer, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("UnnecessaryTemporaryOnConversionFromString")
|
||||
public static boolean isLong(Object object) {
|
||||
return (object instanceof Long)
|
||||
|| (object instanceof String str && isParsable(str, Long::parseLong));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if an object is any type of integer (short, byte, long, or int).
|
||||
*
|
||||
* @param object The object to check.
|
||||
* @return True if the object represents an integer type, false otherwise.
|
||||
*/
|
||||
public static boolean isAnyInteger(Object object) {
|
||||
return isInteger(object) || isShort(object) || isByte(object) || isLong(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies comments from an old node to a new one.
|
||||
*
|
||||
* @param oldNode The original node with comments.
|
||||
* @param newValueNode The new node to which comments should be copied.
|
||||
*/
|
||||
private void copyComments(Node oldNode, Node newValueNode) {
|
||||
if (oldNode == null || newValueNode == null) return;
|
||||
if (oldNode.getBlockComments() != null) {
|
||||
newValueNode.setBlockComments(oldNode.getBlockComments());
|
||||
}
|
||||
if (oldNode.getInLineComments() != null) {
|
||||
newValueNode.setInLineComments(oldNode.getInLineComments());
|
||||
}
|
||||
if (oldNode.getEndComments() != null) {
|
||||
newValueNode.setEndComments(oldNode.getEndComments());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package stirling.software.SPDF.config.interfaces;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.FileInfo;
|
||||
|
||||
public interface DatabaseInterface {
|
||||
|
||||
@@ -69,7 +69,7 @@ public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationF
|
||||
}
|
||||
if (exception instanceof BadCredentialsException
|
||||
|| exception instanceof UsernameNotFoundException) {
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof InternalAuthenticationServiceException
|
||||
|
||||
@@ -14,8 +14,8 @@ import org.springframework.security.saml2.provider.service.authentication.Saml2A
|
||||
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||
|
||||
import com.coveo.saml.SamlClient;
|
||||
import com.coveo.saml.SamlException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
@@ -28,79 +28,61 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.SPDF.model.Provider;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||
import stirling.software.SPDF.utils.UrlUtils;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
|
||||
public static final String LOGOUT_PATH = "/login?logout=true";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Override
|
||||
public void onLogoutSuccess(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
throws IOException, ServletException {
|
||||
|
||||
throws IOException {
|
||||
if (!response.isCommitted()) {
|
||||
// Handle user logout due to disabled account
|
||||
if (request.getParameter("userIsDisabled") != null) {
|
||||
response.sendRedirect(
|
||||
request.getContextPath() + "/login?erroroauth=userIsDisabled");
|
||||
return;
|
||||
}
|
||||
// Handle OAuth2 authentication error
|
||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||
response.sendRedirect(
|
||||
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
|
||||
return;
|
||||
}
|
||||
if (authentication != null) {
|
||||
// Handle SAML2 logout redirection
|
||||
if (authentication instanceof Saml2Authentication) {
|
||||
getRedirect_saml2(request, response, authentication);
|
||||
return;
|
||||
}
|
||||
// Handle OAuth2 logout redirection
|
||||
else if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
getRedirect_oauth2(request, response, authentication);
|
||||
return;
|
||||
}
|
||||
// Handle Username/Password logout
|
||||
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||
return;
|
||||
}
|
||||
// Handle unknown authentication types
|
||||
else {
|
||||
if (authentication instanceof Saml2Authentication samlAuthentication) {
|
||||
// Handle SAML2 logout redirection
|
||||
getRedirect_saml2(request, response, samlAuthentication);
|
||||
} else if (authentication instanceof OAuth2AuthenticationToken oAuthToken) {
|
||||
// Handle OAuth2 logout redirection
|
||||
getRedirect_oauth2(request, response, oAuthToken);
|
||||
} else if (authentication instanceof UsernamePasswordAuthenticationToken) {
|
||||
// Handle Username/Password logout
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
} else {
|
||||
// Handle unknown authentication types
|
||||
log.error(
|
||||
"authentication class unknown: "
|
||||
+ authentication.getClass().getSimpleName());
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||
return;
|
||||
"Authentication class unknown: {}",
|
||||
authentication.getClass().getSimpleName());
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
} else {
|
||||
// Redirect to login page after logout
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||
return;
|
||||
String path = checkForErrors(request);
|
||||
getRedirectStrategy().sendRedirect(request, response, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
String nameIdValue = principal.getName();
|
||||
String nameIdValue = principal.name();
|
||||
|
||||
try {
|
||||
// Read certificate from the resource
|
||||
@@ -111,27 +93,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
certificates.add(certificate);
|
||||
|
||||
// Construct URLs required for SAML configuration
|
||||
String serverUrl =
|
||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||
|
||||
String idpUrl = samlConf.getIdpSingleLogoutUrl();
|
||||
|
||||
String idpIssuer = samlConf.getIdpIssuer();
|
||||
|
||||
// Create SamlClient instance for SAML logout
|
||||
SamlClient samlClient =
|
||||
new SamlClient(
|
||||
relyingPartyIdentifier,
|
||||
assertionConsumerServiceUrl,
|
||||
idpUrl,
|
||||
idpIssuer,
|
||||
certificates,
|
||||
SamlClient.SamlIdpBinding.POST);
|
||||
SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates);
|
||||
|
||||
// Read private key for service provider
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
@@ -143,96 +105,134 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||
// Redirect to identity provider for logout
|
||||
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
|
||||
} catch (Exception e) {
|
||||
log.error(nameIdValue, e);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
|
||||
log.error(
|
||||
"Error retrieving logout URL from Provider {} for user {}",
|
||||
samlConf.getProvider(),
|
||||
nameIdValue,
|
||||
e);
|
||||
getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect for OAuth2 authentication logout
|
||||
private void getRedirect_oauth2(
|
||||
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
OAuth2AuthenticationToken oAuthToken)
|
||||
throws IOException {
|
||||
String param = "logout=true";
|
||||
String registrationId = null;
|
||||
String issuer = null;
|
||||
String clientId = null;
|
||||
String registrationId;
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
String path = checkForErrors(request);
|
||||
|
||||
if (authentication instanceof OAuth2AuthenticationToken) {
|
||||
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
|
||||
registrationId = oauthToken.getAuthorizedClientRegistrationId();
|
||||
|
||||
try {
|
||||
// Get OAuth2 provider details from configuration
|
||||
Provider provider = oauth.getClient().get(registrationId);
|
||||
issuer = provider.getIssuer();
|
||||
clientId = provider.getClientId();
|
||||
} catch (UnsupportedProviderException e) {
|
||||
log.error(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
|
||||
issuer = oauth.getIssuer();
|
||||
clientId = oauth.getClientId();
|
||||
}
|
||||
String errorMessage = "";
|
||||
// Handle different error scenarios during logout
|
||||
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
|
||||
param = "erroroauth=oauth2AuthenticationErrorWeb";
|
||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||
param = "error=" + sanitizeInput(errorMessage);
|
||||
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
|
||||
param = "erroroauth=" + sanitizeInput(errorMessage);
|
||||
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
|
||||
param = "error=oauth2AutoCreateDisabled";
|
||||
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
|
||||
param = "erroroauth=oauth2_admin_blocked_user";
|
||||
} else if (request.getParameter("userIsDisabled") != null) {
|
||||
param = "erroroauth=userIsDisabled";
|
||||
} else if (request.getParameter("badcredentials") != null) {
|
||||
param = "error=badcredentials";
|
||||
}
|
||||
|
||||
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
|
||||
String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path;
|
||||
registrationId = oAuthToken.getAuthorizedClientRegistrationId();
|
||||
|
||||
// Redirect based on OAuth2 provider
|
||||
switch (registrationId.toLowerCase()) {
|
||||
case "keycloak":
|
||||
// Add Keycloak specific logout URL if needed
|
||||
String logoutUrl =
|
||||
issuer
|
||||
+ "/protocol/openid-connect/logout"
|
||||
+ "?client_id="
|
||||
+ clientId
|
||||
+ "&post_logout_redirect_uri="
|
||||
+ response.encodeRedirectURL(redirect_url);
|
||||
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
|
||||
case "keycloak" -> {
|
||||
KeycloakProvider keycloak = oauth.getClient().getKeycloak();
|
||||
|
||||
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);
|
||||
break;
|
||||
case "github":
|
||||
// Add GitHub specific logout URL if needed
|
||||
String githubLogoutUrl = "https://github.com/logout";
|
||||
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
|
||||
response.sendRedirect(githubLogoutUrl);
|
||||
break;
|
||||
case "google":
|
||||
// Add Google specific logout URL if needed
|
||||
// String googleLogoutUrl =
|
||||
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
|
||||
// + response.encodeRedirectURL(redirect_url);
|
||||
log.info("Google does not have a specific logout URL");
|
||||
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
|
||||
// response.sendRedirect(googleLogoutUrl);
|
||||
// break;
|
||||
default:
|
||||
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
|
||||
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
|
||||
response.sendRedirect(defaultRedirectUrl);
|
||||
break;
|
||||
}
|
||||
case "github", "google" -> {
|
||||
log.info(
|
||||
"No redirect URL for {} available. Redirecting to default logout URL: {}",
|
||||
registrationId,
|
||||
redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
default -> {
|
||||
log.info("Redirecting to default logout URL: {}", redirectUrl);
|
||||
response.sendRedirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize input to avoid potential security vulnerabilities
|
||||
private static SamlClient getSamlClient(
|
||||
String registrationId, SAML2 samlConf, List<X509Certificate> certificates)
|
||||
throws SamlException {
|
||||
String serverUrl =
|
||||
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort();
|
||||
|
||||
String relyingPartyIdentifier =
|
||||
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
|
||||
|
||||
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
|
||||
|
||||
String idpSLOUrl = samlConf.getIdpSingleLogoutUrl();
|
||||
|
||||
String idpIssuer = samlConf.getIdpIssuer();
|
||||
|
||||
// Create SamlClient instance for SAML logout
|
||||
return new SamlClient(
|
||||
relyingPartyIdentifier,
|
||||
assertionConsumerServiceUrl,
|
||||
idpSLOUrl,
|
||||
idpIssuer,
|
||||
certificates,
|
||||
SamlClient.SamlIdpBinding.POST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles different error scenarios during logout. Will return a <code>String</code> containing
|
||||
* the error request parameter.
|
||||
*
|
||||
* @param request the user's <code>HttpServletRequest</code> request.
|
||||
* @return a <code>String</code> containing the error request parameter.
|
||||
*/
|
||||
private String checkForErrors(HttpServletRequest request) {
|
||||
String errorMessage;
|
||||
String path = "logout=true";
|
||||
|
||||
if (request.getParameter("oAuth2AuthenticationErrorWeb") != null) {
|
||||
path = "errorOAuth=userAlreadyExistsWeb";
|
||||
} else if ((errorMessage = request.getParameter("errorOAuth")) != null) {
|
||||
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
||||
} else if (request.getParameter("oAuth2AutoCreateDisabled") != null) {
|
||||
path = "errorOAuth=oAuth2AutoCreateDisabled";
|
||||
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
|
||||
path = "errorOAuth=oAuth2AdminBlockedUser";
|
||||
} else if (request.getParameter("userIsDisabled") != null) {
|
||||
path = "errorOAuth=userIsDisabled";
|
||||
} else if ((errorMessage = request.getParameter("error")) != null) {
|
||||
path = "errorOAuth=" + sanitizeInput(errorMessage);
|
||||
} else if (request.getParameter("badCredentials") != null) {
|
||||
path = "errorOAuth=badCredentials";
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize input to avoid potential security vulnerabilities. Will return a sanitised <code>
|
||||
* String</code>.
|
||||
*
|
||||
* @return a sanitised <code>String</code>
|
||||
*/
|
||||
private String sanitizeInput(String input) {
|
||||
return input.replaceAll("[^a-zA-Z0-9 ]", "");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -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();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -51,11 +51,7 @@ public class SecurityConfiguration {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
private final UserService userService;
|
||||
|
||||
@Qualifier("loginEnabled")
|
||||
private final boolean loginEnabledValue;
|
||||
|
||||
@Qualifier("runningEE")
|
||||
private final boolean runningEE;
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@@ -109,6 +105,7 @@ public class SecurityConfiguration {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
@@ -164,8 +161,7 @@ public class SecurityConfiguration {
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(applicationProperties))
|
||||
.clearAuthentication(true)
|
||||
.invalidateHttpSession( // Invalidate session
|
||||
true)
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID", "remember-me"));
|
||||
http.rememberMe(
|
||||
rememberMeConfigurer -> // Use the configurator directly
|
||||
@@ -227,14 +223,14 @@ public class SecurityConfiguration {
|
||||
.permitAll());
|
||||
}
|
||||
// Handle OAUTH2 Logins
|
||||
if (applicationProperties.getSecurity().isOauth2Activ()) {
|
||||
if (applicationProperties.getSecurity().isOauth2Active()) {
|
||||
http.oauth2Login(
|
||||
oauth2 ->
|
||||
oauth2.loginPage("/oauth2")
|
||||
.
|
||||
/*
|
||||
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
|
||||
If user exists, login proceeds as usual. If user does not exist, then it is auto-created but only if 'OAUTH2AutoCreateUser'
|
||||
is set as true, else login fails with an error message advising the same.
|
||||
*/
|
||||
successHandler(
|
||||
@@ -258,8 +254,7 @@ public class SecurityConfiguration {
|
||||
.permitAll());
|
||||
}
|
||||
// Handle SAML
|
||||
if (applicationProperties.getSecurity().isSaml2Activ()) {
|
||||
// && runningEE
|
||||
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
|
||||
// Configure the authentication provider
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
@@ -284,12 +279,13 @@ public class SecurityConfiguration {
|
||||
.authenticationRequestResolver(
|
||||
saml2AuthenticationRequestResolver);
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML2 login", e);
|
||||
log.error("Error configuring SAML 2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.debug("SAML 2 login is not enabled. Using default.");
|
||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||
}
|
||||
return http.build();
|
||||
@@ -315,7 +311,7 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public boolean activSecurity() {
|
||||
public boolean activeSecurity() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
// Use API key to authenticate. This requires you to have an authentication
|
||||
// provider for API keys.
|
||||
Optional<User> user = userService.getUserByApiKey(apiKey);
|
||||
if (!user.isPresent()) {
|
||||
if (user.isEmpty()) {
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
response.getWriter().write("Invalid API Key.");
|
||||
return;
|
||||
@@ -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).getName();
|
||||
} 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,14 +172,14 @@ 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) {
|
||||
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
|
||||
response.sendRedirect(
|
||||
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
|
||||
request.getContextPath() + "/logout?oAuth2AdminBlockedUser=true");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -193,7 +195,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
// Redirect to logout if credentials are invalid
|
||||
if (!isUserExists && notSsoLogin) {
|
||||
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
|
||||
response.sendRedirect(request.getContextPath() + "/logout?badCredentials=true");
|
||||
return;
|
||||
}
|
||||
if (isUserDisabled) {
|
||||
|
||||
@@ -27,7 +27,7 @@ import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrin
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||
import stirling.software.SPDF.model.*;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.repository.AuthorityRepository;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
|
||||
@@ -78,20 +78,18 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
// Handle OAUTH2 login and user auto creation.
|
||||
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||
public void processSSOPostLogin(String username, boolean autoCreateUser)
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||
if (existingUser.isPresent()) {
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
if (autoCreateUser) {
|
||||
saveUser(username, AuthenticationType.SSO);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Authentication getAuthentication(String apiKey) {
|
||||
@@ -123,12 +121,14 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
public User addApiKeyToUser(String username) {
|
||||
Optional<User> user = findByUsernameIgnoreCase(username);
|
||||
if (user.isPresent()) {
|
||||
user.get().setApiKey(generateApiKey());
|
||||
return userRepository.save(user.get());
|
||||
Optional<User> 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) {
|
||||
@@ -173,6 +173,14 @@ public class UserService implements UserServiceInterface {
|
||||
saveUser(username, authenticationType, Role.USER.getRoleId());
|
||||
}
|
||||
|
||||
private User saveUser(Optional<User> user, String apiKey) {
|
||||
if (user.isPresent()) {
|
||||
user.get().setApiKey(apiKey);
|
||||
return userRepository.save(user.get());
|
||||
}
|
||||
throw new UsernameNotFoundException("User not found");
|
||||
}
|
||||
|
||||
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
||||
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
||||
if (!isUsernameValid(username)) {
|
||||
@@ -373,21 +381,18 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
public void invalidateUserSessions(String username) {
|
||||
String usernameP = "";
|
||||
|
||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||
for (SessionInformation sessionsInformation :
|
||||
sessionRegistry.getAllSessions(principal, false)) {
|
||||
if (principal instanceof UserDetails) {
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
usernameP = userDetails.getUsername();
|
||||
} else if (principal instanceof OAuth2User) {
|
||||
OAuth2User oAuth2User = (OAuth2User) principal;
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
usernameP = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
usernameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
CustomSaml2AuthenticatedPrincipal saml2User =
|
||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
||||
usernameP = saml2User.getName();
|
||||
} else if (principal instanceof String) {
|
||||
usernameP = (String) principal;
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
usernameP = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
usernameP = stringUser;
|
||||
}
|
||||
if (usernameP.equalsIgnoreCase(username)) {
|
||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||
@@ -398,49 +403,56 @@ 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).getName();
|
||||
} else if (principal instanceof String) {
|
||||
return (String) principal;
|
||||
} 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
|
||||
public void syncCustomApiUser(String customApiKey)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
||||
public void syncCustomApiUser(String customApiKey) {
|
||||
if (customApiKey == null || customApiKey.trim().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String username = "CUSTOM_API_USER";
|
||||
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
||||
if (!existingUser.isPresent()) {
|
||||
// Create new user with API role
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(UUID.randomUUID().toString());
|
||||
user.setEnabled(true);
|
||||
user.setFirstLogin(false);
|
||||
user.setAuthenticationType(AuthenticationType.WEB);
|
||||
user.setApiKey(customApiKey);
|
||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||
userRepository.save(user);
|
||||
|
||||
existingUser.ifPresentOrElse(
|
||||
user -> {
|
||||
// Update API key if it has changed
|
||||
User updatedUser = existingUser.get();
|
||||
|
||||
if (!customApiKey.equals(updatedUser.getApiKey())) {
|
||||
updatedUser.setApiKey(customApiKey);
|
||||
userRepository.save(updatedUser);
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Create new user with API role
|
||||
User user = new User();
|
||||
user.setUsername(username);
|
||||
user.setPassword(UUID.randomUUID().toString());
|
||||
user.setEnabled(true);
|
||||
user.setFirstLogin(false);
|
||||
user.setAuthenticationType(AuthenticationType.WEB);
|
||||
user.setApiKey(customApiKey);
|
||||
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
||||
userRepository.save(user);
|
||||
});
|
||||
|
||||
try {
|
||||
databaseService.exportDatabase();
|
||||
} else {
|
||||
// Update API key if it has changed
|
||||
User user = existingUser.get();
|
||||
if (!customApiKey.equals(user.getApiKey())) {
|
||||
user.setApiKey(customApiKey);
|
||||
userRepository.save(user);
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
} catch (SQLException | UnsupportedProviderException e) {
|
||||
log.error("Error exporting database after synchronising custom API user", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -14,7 +12,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||
import stirling.software.SPDF.controller.api.H2SQLCondition;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
|
||||
@Component
|
||||
@Conditional(H2SQLCondition.class)
|
||||
|
||||
@@ -29,7 +29,7 @@ public class CustomOAuth2AuthenticationFailureHandler
|
||||
|
||||
if (exception instanceof BadCredentialsException) {
|
||||
log.error("BadCredentialsException", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?error=badCredentials");
|
||||
return;
|
||||
}
|
||||
if (exception instanceof DisabledException) {
|
||||
@@ -42,18 +42,20 @@ 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";
|
||||
}
|
||||
log.error("OAuth2 Authentication error: " + errorCode);
|
||||
log.error("OAuth2AuthenticationException", exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
|
||||
return;
|
||||
|
||||
log.error(
|
||||
"OAuth2 Authentication error: {}",
|
||||
errorCode != null ? errorCode : exception.getMessage(),
|
||||
exception);
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?errorOAuth=" + errorCode);
|
||||
}
|
||||
log.error("Unhandled authentication exception", exception);
|
||||
super.onAuthenticationFailure(request, response, exception);
|
||||
|
||||
@@ -20,19 +20,18 @@ import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
public class CustomOAuth2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private UserService userService;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final UserService userService;
|
||||
|
||||
public CustomOAuth2AuthenticationSuccessHandler(
|
||||
final LoginAttemptService loginAttemptService,
|
||||
LoginAttemptService loginAttemptService,
|
||||
ApplicationProperties applicationProperties,
|
||||
UserService userService) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
@@ -48,12 +47,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
Object principal = authentication.getPrincipal();
|
||||
String username = "";
|
||||
|
||||
if (principal instanceof OAuth2User) {
|
||||
OAuth2User oauthUser = (OAuth2User) principal;
|
||||
username = oauthUser.getName();
|
||||
} else if (principal instanceof UserDetails) {
|
||||
UserDetails oauthUser = (UserDetails) principal;
|
||||
username = oauthUser.getUsername();
|
||||
if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
} else if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
}
|
||||
|
||||
// Get the saved request
|
||||
@@ -78,6 +75,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
}
|
||||
|
||||
if (userService.isUserDisabled(username)) {
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||
@@ -87,13 +85,14 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||
&& oAuth.getAutoCreateUser()) {
|
||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
response.sendRedirect(contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (oAuth.getBlockRegistration()
|
||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
|
||||
return;
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
|
||||
@@ -17,19 +17,19 @@ import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
|
||||
@Slf4j
|
||||
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
|
||||
|
||||
private final OidcUserService delegate = new OidcUserService();
|
||||
|
||||
private UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
private final LoginAttemptService loginAttemptService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public CustomOAuth2UserService(
|
||||
ApplicationProperties applicationProperties,
|
||||
@@ -42,34 +42,26 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
|
||||
@Override
|
||||
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
|
||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||
String usernameAttribute = oauth2.getUseAsUsername();
|
||||
if (usernameAttribute == null || usernameAttribute.trim().isEmpty()) {
|
||||
Client client = oauth2.getClient();
|
||||
if (client != null && client.getKeycloak() != null) {
|
||||
usernameAttribute = client.getKeycloak().getUseAsUsername();
|
||||
} else {
|
||||
usernameAttribute = "email";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
OidcUser user = delegate.loadUser(userRequest);
|
||||
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
|
||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||
UsernameAttribute usernameAttribute =
|
||||
UsernameAttribute.valueOf(oauth2.getUseAsUsername().toUpperCase());
|
||||
String usernameAttributeKey = usernameAttribute.getName();
|
||||
|
||||
// Check if the username claim is null or empty
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Claim '" + usernameAttribute + "' cannot be null or empty");
|
||||
}
|
||||
// todo: save user by OIDC ID instead of username
|
||||
Optional<User> internalUser =
|
||||
userService.findByUsernameIgnoreCase(user.getAttribute(usernameAttributeKey));
|
||||
|
||||
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
|
||||
if (duser.isPresent()) {
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
if (internalUser.isPresent()) {
|
||||
String internalUsername = internalUser.get().getUsername();
|
||||
if (loginAttemptService.isBlocked(internalUsername)) {
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
"The account "
|
||||
+ internalUsername
|
||||
+ " has been locked due to too many failed login attempts.");
|
||||
}
|
||||
if (userService.hasPassword(username)) {
|
||||
if (userService.hasPassword(usernameAttributeKey)) {
|
||||
throw new IllegalArgumentException("Password must not be null");
|
||||
}
|
||||
}
|
||||
@@ -79,7 +71,7 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
|
||||
user.getAuthorities(),
|
||||
userRequest.getIdToken(),
|
||||
user.getUserInfo(),
|
||||
usernameAttribute);
|
||||
usernameAttributeKey);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error loading OIDC user: {}", e.getMessage());
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package stirling.software.SPDF.config.security.oauth2;
|
||||
|
||||
import static org.springframework.security.oauth2.core.AuthorizationGrantType.AUTHORIZATION_CODE;
|
||||
import static stirling.software.SPDF.utils.validation.Validator.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -26,18 +29,20 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
import stirling.software.SPDF.model.exception.NoProviderFoundException;
|
||||
import stirling.software.SPDF.model.provider.GitHubProvider;
|
||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||
import stirling.software.SPDF.model.provider.Provider;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@Configuration
|
||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||
public class OAuth2Configuration {
|
||||
|
||||
public static final String REDIRECT_URI_PATH = "{baseUrl}/login/oauth2/code/";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
@Lazy private final UserService userService;
|
||||
|
||||
@@ -48,139 +53,175 @@ public class OAuth2Configuration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||
public ClientRegistrationRepository clientRegistrationRepository()
|
||||
throws NoProviderFoundException {
|
||||
List<ClientRegistration> registrations = new ArrayList<>();
|
||||
githubClientRegistration().ifPresent(registrations::add);
|
||||
oidcClientRegistration().ifPresent(registrations::add);
|
||||
googleClientRegistration().ifPresent(registrations::add);
|
||||
keycloakClientRegistration().ifPresent(registrations::add);
|
||||
|
||||
if (registrations.isEmpty()) {
|
||||
log.error("At least one OAuth2 provider must be configured");
|
||||
System.exit(1);
|
||||
log.error("No OAuth2 provider registered");
|
||||
throw new NoProviderFoundException("At least one OAuth2 provider must be configured.");
|
||||
}
|
||||
|
||||
return new InMemoryClientRegistrationRepository(registrations);
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> googleClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
if (oauth == null || !oauth.getEnabled()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GoogleProvider google = client.getGoogle();
|
||||
return google != null && google.isSettingsValid()
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId(google.getName())
|
||||
.clientId(google.getClientId())
|
||||
.clientSecret(google.getClientSecret())
|
||||
.scope(google.getScopes())
|
||||
.authorizationUri(google.getAuthorizationuri())
|
||||
.tokenUri(google.getTokenuri())
|
||||
.userInfoUri(google.getUserinfouri())
|
||||
.userNameAttributeName(google.getUseAsUsername())
|
||||
.clientName(google.getClientName())
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
|
||||
.authorizationGrantType(
|
||||
org.springframework.security.oauth2.core
|
||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> keycloakClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
if (oauth == null || !oauth.getEnabled()) {
|
||||
OAUTH2 oauth2 = applicationProperties.getSecurity().getOauth2();
|
||||
|
||||
if (isOAuth2Enabled(oauth2) || isClientInitialised(oauth2)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
return keycloak != null && keycloak.isSettingsValid()
|
||||
|
||||
Client client = oauth2.getClient();
|
||||
KeycloakProvider keycloakClient = client.getKeycloak();
|
||||
Provider keycloak =
|
||||
new KeycloakProvider(
|
||||
keycloakClient.getIssuer(),
|
||||
keycloakClient.getClientId(),
|
||||
keycloakClient.getClientSecret(),
|
||||
keycloakClient.getScopes(),
|
||||
keycloakClient.getUseAsUsername());
|
||||
|
||||
return validateProvider(keycloak)
|
||||
? Optional.of(
|
||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
||||
.registrationId(keycloak.getName())
|
||||
.clientId(keycloak.getClientId())
|
||||
.clientSecret(keycloak.getClientSecret())
|
||||
.scope(keycloak.getScopes())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
||||
.userNameAttributeName(keycloak.getUseAsUsername().getName())
|
||||
.clientName(keycloak.getClientName())
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> googleClientRegistration() {
|
||||
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
||||
|
||||
if (isOAuth2Enabled(oAuth2) || isClientInitialised(oAuth2)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
Client client = oAuth2.getClient();
|
||||
GoogleProvider googleClient = client.getGoogle();
|
||||
Provider google =
|
||||
new GoogleProvider(
|
||||
googleClient.getClientId(),
|
||||
googleClient.getClientSecret(),
|
||||
googleClient.getScopes(),
|
||||
googleClient.getUseAsUsername());
|
||||
|
||||
return validateProvider(google)
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId(google.getName())
|
||||
.clientId(google.getClientId())
|
||||
.clientSecret(google.getClientSecret())
|
||||
.scope(google.getScopes())
|
||||
.authorizationUri(google.getAuthorizationUri())
|
||||
.tokenUri(google.getTokenUri())
|
||||
.userInfoUri(google.getUserInfoUri())
|
||||
.userNameAttributeName(google.getUseAsUsername().getName())
|
||||
.clientName(google.getClientName())
|
||||
.redirectUri(REDIRECT_URI_PATH + google.getName())
|
||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> githubClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
if (oauth == null || !oauth.getEnabled()) {
|
||||
OAUTH2 oAuth2 = applicationProperties.getSecurity().getOauth2();
|
||||
|
||||
if (isOAuth2Enabled(oAuth2)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Client client = oauth.getClient();
|
||||
if (client == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
GithubProvider github = client.getGithub();
|
||||
return github != null && github.isSettingsValid()
|
||||
|
||||
Client client = oAuth2.getClient();
|
||||
GitHubProvider githubClient = client.getGithub();
|
||||
Provider github =
|
||||
new GitHubProvider(
|
||||
githubClient.getClientId(),
|
||||
githubClient.getClientSecret(),
|
||||
githubClient.getScopes(),
|
||||
githubClient.getUseAsUsername());
|
||||
|
||||
return validateProvider(github)
|
||||
? Optional.of(
|
||||
ClientRegistration.withRegistrationId(github.getName())
|
||||
.clientId(github.getClientId())
|
||||
.clientSecret(github.getClientSecret())
|
||||
.scope(github.getScopes())
|
||||
.authorizationUri(github.getAuthorizationuri())
|
||||
.tokenUri(github.getTokenuri())
|
||||
.userInfoUri(github.getUserinfouri())
|
||||
.userNameAttributeName(github.getUseAsUsername())
|
||||
.authorizationUri(github.getAuthorizationUri())
|
||||
.tokenUri(github.getTokenUri())
|
||||
.userInfoUri(github.getUserInfoUri())
|
||||
.userNameAttributeName(github.getUseAsUsername().getName())
|
||||
.clientName(github.getClientName())
|
||||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
||||
.authorizationGrantType(
|
||||
org.springframework.security.oauth2.core
|
||||
.AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.redirectUri(REDIRECT_URI_PATH + github.getName())
|
||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private Optional<ClientRegistration> oidcClientRegistration() {
|
||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
||||
if (oauth == null
|
||||
|| oauth.getIssuer() == null
|
||||
|| oauth.getIssuer().isEmpty()
|
||||
|| oauth.getClientId() == null
|
||||
|| oauth.getClientId().isEmpty()
|
||||
|| oauth.getClientSecret() == null
|
||||
|| oauth.getClientSecret().isEmpty()
|
||||
|| oauth.getScopes() == null
|
||||
|| oauth.getScopes().isEmpty()
|
||||
|| oauth.getUseAsUsername() == null
|
||||
|| oauth.getUseAsUsername().isEmpty()) {
|
||||
|
||||
if (isOAuth2Enabled(oauth) || isClientInitialised(oauth)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(
|
||||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||
.registrationId("oidc")
|
||||
.clientId(oauth.getClientId())
|
||||
.clientSecret(oauth.getClientSecret())
|
||||
.scope(oauth.getScopes())
|
||||
.userNameAttributeName(oauth.getUseAsUsername())
|
||||
.clientName("OIDC")
|
||||
.build());
|
||||
|
||||
String name = oauth.getProvider();
|
||||
String firstChar = String.valueOf(name.charAt(0));
|
||||
String clientName = name.replaceFirst(firstChar, firstChar.toUpperCase());
|
||||
|
||||
Provider oidcProvider =
|
||||
new Provider(
|
||||
oauth.getIssuer(),
|
||||
name,
|
||||
clientName,
|
||||
oauth.getClientId(),
|
||||
oauth.getClientSecret(),
|
||||
oauth.getScopes(),
|
||||
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
|
||||
oauth.getLogoutUrl(),
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
return !isStringEmpty(oidcProvider.getIssuer()) || validateProvider(oidcProvider)
|
||||
? Optional.of(
|
||||
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
|
||||
.registrationId(name)
|
||||
.clientId(oidcProvider.getClientId())
|
||||
.clientSecret(oidcProvider.getClientSecret())
|
||||
.scope(oidcProvider.getScopes())
|
||||
.userNameAttributeName(oidcProvider.getUseAsUsername().getName())
|
||||
.clientName(clientName)
|
||||
.redirectUri(REDIRECT_URI_PATH + "oidc")
|
||||
.authorizationGrantType(AUTHORIZATION_CODE)
|
||||
.build())
|
||||
: Optional.empty();
|
||||
}
|
||||
|
||||
private boolean isOAuth2Enabled(OAUTH2 oAuth2) {
|
||||
return oAuth2 == null || !oAuth2.getEnabled();
|
||||
}
|
||||
|
||||
private boolean isClientInitialised(OAUTH2 oauth2) {
|
||||
Client client = oauth2.getClient();
|
||||
return client == null;
|
||||
}
|
||||
|
||||
/*
|
||||
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
|
||||
This is required for the internal; 'hasRole()' function to give out the correct role.
|
||||
*/
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@ConditionalOnProperty(value = "security.oauth2.enabled", havingValue = "true")
|
||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
||||
return (authorities) -> {
|
||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
||||
@@ -189,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()
|
||||
@@ -197,14 +238,12 @@ public class OAuth2Configuration {
|
||||
.getUseAsUsername();
|
||||
Optional<User> userOpt =
|
||||
userService.findByUsernameIgnoreCase(
|
||||
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
||||
(String) oAuth2Auth.getAttributes().get(useAsUsername));
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user != null) {
|
||||
mappedAuthorities.add(
|
||||
new SimpleGrantedAuthority(
|
||||
userService.findRole(user).getAuthority()));
|
||||
}
|
||||
mappedAuthorities.add(
|
||||
new SimpleGrantedAuthority(
|
||||
userService.findRole(user).getAuthority()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -13,8 +13,10 @@ import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemReader;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public class CertificateUtils {
|
||||
|
||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||
@@ -38,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: "
|
||||
|
||||
@@ -4,27 +4,17 @@ import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
|
||||
|
||||
public class CustomSaml2AuthenticatedPrincipal
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public record CustomSaml2AuthenticatedPrincipal(
|
||||
String name,
|
||||
Map<String, List<Object>> attributes,
|
||||
String nameId,
|
||||
List<String> sessionIndexes)
|
||||
implements Saml2AuthenticatedPrincipal, Serializable {
|
||||
|
||||
private final String name;
|
||||
private final Map<String, List<Object>> attributes;
|
||||
private final String nameId;
|
||||
private final List<String> sessionIndexes;
|
||||
|
||||
public CustomSaml2AuthenticatedPrincipal(
|
||||
String name,
|
||||
Map<String, List<Object>> attributes,
|
||||
String nameId,
|
||||
List<String> sessionIndexes) {
|
||||
this.name = name;
|
||||
this.attributes = attributes;
|
||||
this.nameId = nameId;
|
||||
this.sessionIndexes = sessionIndexes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
@@ -34,12 +24,4 @@ public class CustomSaml2AuthenticatedPrincipal
|
||||
public Map<String, List<Object>> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
public String getNameId() {
|
||||
return this.nameId;
|
||||
}
|
||||
|
||||
public List<String> getSessionIndexes() {
|
||||
return this.sessionIndexes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.security.authentication.ProviderNotFoundException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.saml2.core.Saml2Error;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
@@ -22,18 +23,19 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
throws IOException {
|
||||
log.error("Authentication error", exception);
|
||||
|
||||
if (exception instanceof Saml2AuthenticationException) {
|
||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||
.sendRedirect(request, response, "/login?errorOAuth=" + error.getErrorCode());
|
||||
} else if (exception instanceof ProviderNotFoundException) {
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?erroroauth=not_authentication_provider_found");
|
||||
"/login?errorOAuth=not_authentication_provider_found");
|
||||
}
|
||||
log.error("AuthenticationException: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -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).getName();
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2Principal) {
|
||||
String username = saml2Principal.name();
|
||||
log.debug("Authenticated principal found for user: {}", username);
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
@@ -97,7 +97,7 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
contextPath + "/logout?oAuth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,20 +105,18 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
if (saml2.getBlockRegistration() && !userExists) {
|
||||
log.debug("Registration blocked for new user: {}", username);
|
||||
response.sendRedirect(
|
||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
|
||||
return;
|
||||
}
|
||||
log.debug("Processing SSO post-login for user: {}", username);
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
import org.opensaml.saml.saml2.core.AuthnStatement;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||
@@ -18,10 +19,11 @@ import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.User;
|
||||
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public class CustomSaml2ResponseAuthenticationConverter
|
||||
implements Converter<ResponseToken, Saml2Authentication> {
|
||||
|
||||
private UserService userService;
|
||||
private final UserService userService;
|
||||
|
||||
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
|
||||
this.userService = userService;
|
||||
@@ -61,10 +63,10 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Debug log with actual values
|
||||
log.debug("Extracted SAML Attributes: " + attributes);
|
||||
log.debug("Extracted SAML Attributes: {}", attributes);
|
||||
|
||||
// Try to get username/identifier in order of preference
|
||||
String userIdentifier = null;
|
||||
String userIdentifier;
|
||||
if (hasAttribute(attributes, "username")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||
@@ -84,10 +86,8 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user != null) {
|
||||
simpleGrantedAuthority =
|
||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||
}
|
||||
simpleGrantedAuthority =
|
||||
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
|
||||
}
|
||||
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
@@ -102,7 +102,7 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
return new Saml2Authentication(
|
||||
principal,
|
||||
responseToken.getToken().getSaml2Response(),
|
||||
Collections.singletonList(simpleGrantedAuthority));
|
||||
List.of(simpleGrantedAuthority));
|
||||
}
|
||||
|
||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||
|
||||
@@ -11,10 +11,12 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
|
||||
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -26,27 +28,20 @@ import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
value = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@ConditionalOnProperty(value = "security.saml2.enabled", havingValue = "true")
|
||||
public class SAML2Configuration {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public SAML2Configuration(ApplicationProperties applicationProperties) {
|
||||
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getIdpCert());
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
@@ -58,81 +53,124 @@ public class SAML2Configuration {
|
||||
RelyingPartyRegistration rp =
|
||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||
.signingX509Credentials(c -> c.add(signingCredential))
|
||||
.entityId(samlConf.getIdpIssuer())
|
||||
.singleLogoutServiceBinding(Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(samlConf.getIdpSingleLogoutUrl())
|
||||
.singleLogoutServiceResponseLocation("http://localhost:8080/login")
|
||||
.assertionConsumerServiceBinding(Saml2MessageBinding.POST)
|
||||
.assertionConsumerServiceLocation(
|
||||
"{baseUrl}/login/saml2/sso/{registrationId}")
|
||||
.assertingPartyMetadata(
|
||||
metadata ->
|
||||
metadata.entityId(samlConf.getIdpIssuer())
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.verificationX509Credentials(
|
||||
c -> c.add(verificationCredential))
|
||||
.singleSignOnServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.singleLogoutServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
.singleLogoutServiceLocation(
|
||||
samlConf.getIdpSingleLogoutUrl())
|
||||
.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
@ConditionalOnProperty(name = "security.saml2.enabled", havingValue = "true")
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
|
||||
resolver.setAuthnRequestCustomizer(
|
||||
customizer -> {
|
||||
log.debug("Customizing SAML Authentication request");
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||
}
|
||||
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||
log.debug(
|
||||
"AuthnRequest Issuer: {}",
|
||||
authnRequest.getIssuer() != null
|
||||
? authnRequest.getIssuer().getValue()
|
||||
: "null");
|
||||
HttpServletRequest request = customizer.getRequest();
|
||||
// Log HTTP request details
|
||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||
log.debug("Request URI: {}", request.getRequestURI());
|
||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||
log.debug("Query String: {}", request.getQueryString());
|
||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||
// Log headers
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName -> {
|
||||
log.debug(
|
||||
"Header - {}: {}",
|
||||
headerName,
|
||||
request.getHeader(headerName));
|
||||
});
|
||||
// Log SAML specific parameters
|
||||
log.debug("SAML Request Parameters:");
|
||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||
// Log session debugrmation if exists
|
||||
if (request.getSession(false) != null) {
|
||||
log.debug("Session ID: {}", request.getSession().getId());
|
||||
}
|
||||
// Log any assertions consumer service details if present
|
||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
HttpSessionSaml2AuthenticationRequestRepository requestRepository =
|
||||
new HttpSessionSaml2AuthenticationRequestRepository();
|
||||
AbstractSaml2AuthenticationRequest saml2AuthenticationRequest =
|
||||
requestRepository.loadAuthenticationRequest(request);
|
||||
|
||||
if (saml2AuthenticationRequest != null) {
|
||||
String sessionId = request.getSession(false).getId();
|
||||
|
||||
log.debug(
|
||||
"AssertionConsumerServiceURL: {}",
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
}
|
||||
// Log NameID policy if present
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug(
|
||||
"NameIDPolicy Format: {}",
|
||||
authnRequest.getNameIDPolicy().getFormat());
|
||||
"Retrieving SAML 2 authentication request ID from the current HTTP session {}",
|
||||
sessionId);
|
||||
|
||||
String authenticationRequestId = saml2AuthenticationRequest.getId();
|
||||
|
||||
if (!authenticationRequestId.isBlank()) {
|
||||
authnRequest.setID(authenticationRequestId);
|
||||
} else {
|
||||
log.warn(
|
||||
"No authentication request found for HTTP session {}. Generating new ID",
|
||||
sessionId);
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
} else {
|
||||
log.debug("Generating new authentication request ID");
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString().substring(1));
|
||||
}
|
||||
|
||||
logAuthnRequestDetails(authnRequest);
|
||||
logHttpRequestDetails(request);
|
||||
});
|
||||
return resolver;
|
||||
}
|
||||
|
||||
private static void logAuthnRequestDetails(AuthnRequest authnRequest) {
|
||||
String message =
|
||||
"""
|
||||
AuthnRequest:
|
||||
|
||||
ID: {}
|
||||
Issuer: {}
|
||||
IssueInstant: {}
|
||||
AssertionConsumerService (ACS) URL: {}
|
||||
""";
|
||||
log.debug(
|
||||
message,
|
||||
authnRequest.getID(),
|
||||
authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : null,
|
||||
authnRequest.getIssueInstant(),
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug("NameIDPolicy Format: {}", authnRequest.getNameIDPolicy().getFormat());
|
||||
}
|
||||
}
|
||||
|
||||
private static void logHttpRequestDetails(HttpServletRequest request) {
|
||||
log.debug("HTTP Headers: ");
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName ->
|
||||
log.debug("{}: {}", headerName, request.getHeader(headerName)));
|
||||
String message =
|
||||
"""
|
||||
HTTP Request Method: {}
|
||||
Session ID: {}
|
||||
Request Path: {}
|
||||
Query String: {}
|
||||
Remote Address: {}
|
||||
|
||||
SAML Request Parameters:
|
||||
|
||||
SAMLRequest: {}
|
||||
RelayState: {}
|
||||
""";
|
||||
log.debug(
|
||||
message,
|
||||
request.getMethod(),
|
||||
request.getSession().getId(),
|
||||
request.getRequestURI(),
|
||||
request.getQueryString(),
|
||||
request.getRemoteAddr(),
|
||||
request.getParameter("SAMLRequest"),
|
||||
request.getParameter("RelayState"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
List<SessionInformation> 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).getName();
|
||||
} 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).getName();
|
||||
} 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) {
|
||||
|
||||
@@ -31,14 +31,14 @@ public class SettingsController {
|
||||
@PostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "User", description = "User APIs")
|
||||
@@ -126,7 +126,7 @@ public class UserController {
|
||||
return new RedirectView("/change-creds?messageType=notAuthenticated", true);
|
||||
}
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||
if (userOpt == null || userOpt.isEmpty()) {
|
||||
if (userOpt.isEmpty()) {
|
||||
return new RedirectView("/change-creds?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
@@ -154,7 +154,7 @@ public class UserController {
|
||||
return new RedirectView("/account?messageType=notAuthenticated", true);
|
||||
}
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
|
||||
if (userOpt == null || userOpt.isEmpty()) {
|
||||
if (userOpt.isEmpty()) {
|
||||
return new RedirectView("/account?messageType=userNotFound", true);
|
||||
}
|
||||
User user = userOpt.get();
|
||||
@@ -176,7 +176,7 @@ public class UserController {
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||
}
|
||||
log.debug("Processed updates: " + updates);
|
||||
log.debug("Processed updates: {}", updates);
|
||||
// Assuming you have a method in userService to update the settings for a user
|
||||
userService.updateUserSettings(principal.getName(), updates);
|
||||
// Redirect to a page of your choice after updating
|
||||
@@ -199,7 +199,7 @@ public class UserController {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
if (user != null && user.getUsername().equalsIgnoreCase(username)) {
|
||||
if (user.getUsername().equalsIgnoreCase(username)) {
|
||||
return new RedirectView("/addUsers?messageType=usernameExists", true);
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ public class UserController {
|
||||
Authentication authentication)
|
||||
throws SQLException, UnsupportedProviderException {
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
|
||||
if (!userOpt.isPresent()) {
|
||||
if (userOpt.isEmpty()) {
|
||||
return new RedirectView("/addUsers?messageType=userNotFound", true);
|
||||
}
|
||||
if (!userService.usernameExistsIgnoreCase(username)) {
|
||||
@@ -295,20 +295,20 @@ public class UserController {
|
||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||
String userNameP = "";
|
||||
for (Object principal : principals) {
|
||||
List<SessionInformation> sessionsInformations =
|
||||
List<SessionInformation> 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).getName();
|
||||
} 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 sessionsInformation : sessionsInformations) {
|
||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||
for (SessionInformation sessionInfo : sessionsInformation) {
|
||||
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
|
||||
throws IOException {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
@@ -52,7 +52,8 @@ public class CompressController {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor, boolean grayScale) throws Exception {
|
||||
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor, boolean grayScale)
|
||||
throws Exception {
|
||||
byte[] fileBytes = Files.readAllBytes(pdfFile);
|
||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||
double scaleFactor = initialScaleFactor;
|
||||
@@ -62,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);
|
||||
@@ -118,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<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
@@ -220,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,8 @@ public class PipelineProcessor {
|
||||
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("fileInput", file);
|
||||
for (Entry<String, Object> 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<String, Object> 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<byte[]> 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
|
||||
|
||||
@@ -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<byte[]> 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();
|
||||
}
|
||||
|
||||
@@ -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<String> 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<COSName> 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
|
||||
|
||||
@@ -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<byte[]> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
package stirling.software.SPDF.controller.web;
|
||||
|
||||
import static stirling.software.SPDF.utils.validation.Validator.validateProvider;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -24,12 +31,16 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.model.*;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
|
||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||
import stirling.software.SPDF.model.Authority;
|
||||
import stirling.software.SPDF.model.Role;
|
||||
import stirling.software.SPDF.model.SessionEntity;
|
||||
import stirling.software.SPDF.model.User;
|
||||
import stirling.software.SPDF.model.provider.GitHubProvider;
|
||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||
import stirling.software.SPDF.repository.UserRepository;
|
||||
@@ -39,12 +50,12 @@ import stirling.software.SPDF.repository.UserRepository;
|
||||
@Tag(name = "Account Security", description = "Account Security APIs")
|
||||
public class AccountWebController {
|
||||
|
||||
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
|
||||
private final UserRepository // Assuming you have a repository for user operations
|
||||
userRepository;
|
||||
// Assuming you have a repository for user operations
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public AccountWebController(
|
||||
ApplicationProperties applicationProperties,
|
||||
@@ -61,132 +72,125 @@ public class AccountWebController {
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
Map<String, String> providerList = new HashMap<>();
|
||||
Security securityProps = applicationProperties.getSecurity();
|
||||
OAUTH2 oauth = securityProps.getOauth2();
|
||||
|
||||
if (oauth != null) {
|
||||
if (oauth.getEnabled()) {
|
||||
if (oauth.isSettingsValid()) {
|
||||
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
|
||||
String firstChar = String.valueOf(oauth.getProvider().charAt(0));
|
||||
String clientName =
|
||||
oauth.getProvider().replaceFirst(firstChar, firstChar.toUpperCase());
|
||||
providerList.put(OAUTH_2_AUTHORIZATION + oauth.getProvider(), clientName);
|
||||
}
|
||||
|
||||
Client client = oauth.getClient();
|
||||
|
||||
if (client != null) {
|
||||
GoogleProvider google = client.getGoogle();
|
||||
if (google.isSettingsValid()) {
|
||||
|
||||
if (validateProvider(google)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + google.getName(),
|
||||
google.getClientName());
|
||||
OAUTH_2_AUTHORIZATION + google.getName(), google.getClientName());
|
||||
}
|
||||
GithubProvider github = client.getGithub();
|
||||
if (github.isSettingsValid()) {
|
||||
|
||||
GitHubProvider github = client.getGithub();
|
||||
|
||||
if (validateProvider(github)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + github.getName(),
|
||||
github.getClientName());
|
||||
OAUTH_2_AUTHORIZATION + github.getName(), github.getClientName());
|
||||
}
|
||||
|
||||
KeycloakProvider keycloak = client.getKeycloak();
|
||||
if (keycloak.isSettingsValid()) {
|
||||
|
||||
if (validateProvider(keycloak)) {
|
||||
providerList.put(
|
||||
"/oauth2/authorization/" + keycloak.getName(),
|
||||
OAUTH_2_AUTHORIZATION + keycloak.getName(),
|
||||
keycloak.getClientName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SAML2 saml2 = securityProps.getSaml2();
|
||||
if (securityProps.isSaml2Activ()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
||||
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
|
||||
|
||||
if (securityProps.isSaml2Active()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||
String samlIdp = saml2.getProvider();
|
||||
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
|
||||
|
||||
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
|
||||
return "redirect:" + request.getRequestURL() + saml2AuthenticationPath;
|
||||
} else {
|
||||
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any null keys/values from the providerList
|
||||
providerList
|
||||
.entrySet()
|
||||
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
|
||||
model.addAttribute("providerlist", providerList);
|
||||
model.addAttribute("providerList", providerList);
|
||||
model.addAttribute("loginMethod", securityProps.getLoginMethod());
|
||||
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
|
||||
|
||||
boolean altLogin = !providerList.isEmpty() ? securityProps.isAltLogin() : false;
|
||||
|
||||
model.addAttribute("altLogin", altLogin);
|
||||
model.addAttribute("currentPage", "login");
|
||||
String error = request.getParameter("error");
|
||||
|
||||
if (error != null) {
|
||||
switch (error) {
|
||||
case "badcredentials":
|
||||
error = "login.invalid";
|
||||
break;
|
||||
case "locked":
|
||||
error = "login.locked";
|
||||
break;
|
||||
case "oauth2AuthenticationError":
|
||||
error = "userAlreadyExistsOAuthMessage";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case "badCredentials" -> error = "login.invalid";
|
||||
case "locked" -> error = "login.locked";
|
||||
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
|
||||
}
|
||||
|
||||
model.addAttribute("error", error);
|
||||
}
|
||||
String erroroauth = request.getParameter("erroroauth");
|
||||
if (erroroauth != null) {
|
||||
switch (erroroauth) {
|
||||
case "oauth2AutoCreateDisabled":
|
||||
erroroauth = "login.oauth2AutoCreateDisabled";
|
||||
break;
|
||||
case "invalidUsername":
|
||||
erroroauth = "login.invalid";
|
||||
break;
|
||||
case "userAlreadyExistsWeb":
|
||||
erroroauth = "userAlreadyExistsWebMessage";
|
||||
break;
|
||||
case "oauth2AuthenticationErrorWeb":
|
||||
erroroauth = "login.oauth2InvalidUserType";
|
||||
break;
|
||||
case "invalid_token_response":
|
||||
erroroauth = "login.oauth2InvalidTokenResponse";
|
||||
break;
|
||||
case "authorization_request_not_found":
|
||||
erroroauth = "login.oauth2RequestNotFound";
|
||||
break;
|
||||
case "access_denied":
|
||||
erroroauth = "login.oauth2AccessDenied";
|
||||
break;
|
||||
case "invalid_user_info_response":
|
||||
erroroauth = "login.oauth2InvalidUserInfoResponse";
|
||||
break;
|
||||
case "invalid_request":
|
||||
erroroauth = "login.oauth2invalidRequest";
|
||||
break;
|
||||
case "invalid_id_token":
|
||||
erroroauth = "login.oauth2InvalidIdToken";
|
||||
break;
|
||||
case "oauth2_admin_blocked_user":
|
||||
erroroauth = "login.oauth2AdminBlockedUser";
|
||||
break;
|
||||
case "userIsDisabled":
|
||||
erroroauth = "login.userIsDisabled";
|
||||
break;
|
||||
case "invalid_destination":
|
||||
erroroauth = "login.invalid_destination";
|
||||
break;
|
||||
case "relying_party_registration_not_found":
|
||||
erroroauth = "login.relyingPartyRegistrationNotFound";
|
||||
break;
|
||||
|
||||
String errorOAuth = request.getParameter("errorOAuth");
|
||||
|
||||
if (errorOAuth != null) {
|
||||
switch (errorOAuth) {
|
||||
case "oAuth2AutoCreateDisabled" -> errorOAuth = "login.oAuth2AutoCreateDisabled";
|
||||
case "invalidUsername" -> errorOAuth = "login.invalid";
|
||||
case "userAlreadyExistsWeb" -> errorOAuth = "userAlreadyExistsWebMessage";
|
||||
case "oAuth2AuthenticationErrorWeb" -> errorOAuth = "login.oauth2InvalidUserType";
|
||||
case "invalid_token_response" -> errorOAuth = "login.oauth2InvalidTokenResponse";
|
||||
case "authorization_request_not_found" ->
|
||||
errorOAuth = "login.oauth2RequestNotFound";
|
||||
case "access_denied" -> errorOAuth = "login.oauth2AccessDenied";
|
||||
case "invalid_user_info_response" ->
|
||||
errorOAuth = "login.oauth2InvalidUserInfoResponse";
|
||||
case "invalid_request" -> errorOAuth = "login.oauth2invalidRequest";
|
||||
case "invalid_id_token" -> errorOAuth = "login.oauth2InvalidIdToken";
|
||||
case "oAuth2AdminBlockedUser" -> errorOAuth = "login.oAuth2AdminBlockedUser";
|
||||
case "userIsDisabled" -> errorOAuth = "login.userIsDisabled";
|
||||
case "invalid_destination" -> errorOAuth = "login.invalid_destination";
|
||||
case "relying_party_registration_not_found" ->
|
||||
errorOAuth = "login.relyingPartyRegistrationNotFound";
|
||||
// Valid InResponseTo was not available from the validation context, unable to
|
||||
// evaluate
|
||||
case "invalid_in_response_to":
|
||||
erroroauth = "login.invalid_in_response_to";
|
||||
break;
|
||||
case "not_authentication_provider_found":
|
||||
erroroauth = "login.not_authentication_provider_found";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case "invalid_in_response_to" -> errorOAuth = "login.invalid_in_response_to";
|
||||
case "not_authentication_provider_found" ->
|
||||
errorOAuth = "login.not_authentication_provider_found";
|
||||
}
|
||||
model.addAttribute("erroroauth", erroroauth);
|
||||
|
||||
model.addAttribute("errorOAuth", errorOAuth);
|
||||
}
|
||||
|
||||
if (request.getParameter("messageType") != null) {
|
||||
model.addAttribute("messageType", "changedCredsMessage");
|
||||
}
|
||||
|
||||
if (request.getParameter("logout") != null) {
|
||||
model.addAttribute("logoutMessage", "You have been logged out.");
|
||||
}
|
||||
|
||||
return "login";
|
||||
}
|
||||
|
||||
@@ -230,13 +234,11 @@ public class AccountWebController {
|
||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||
hasActiveSession = false;
|
||||
} else {
|
||||
hasActiveSession = !sessionEntity.isExpired();
|
||||
}
|
||||
lastRequest = sessionEntity.getLastRequest();
|
||||
} else {
|
||||
hasActiveSession = false;
|
||||
// No session, set default last request time
|
||||
lastRequest = new Date(0);
|
||||
}
|
||||
@@ -273,53 +275,41 @@ public class AccountWebController {
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
String messageType = request.getParameter("messageType");
|
||||
String deleteMessage = null;
|
||||
|
||||
String deleteMessage;
|
||||
if (messageType != null) {
|
||||
switch (messageType) {
|
||||
case "deleteCurrentUser":
|
||||
deleteMessage = "deleteCurrentUserMessage";
|
||||
break;
|
||||
case "deleteUsernameExists":
|
||||
deleteMessage = "deleteUsernameExistsMessage";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
deleteMessage =
|
||||
switch (messageType) {
|
||||
case "deleteCurrentUser" -> "deleteCurrentUserMessage";
|
||||
case "deleteUsernameExists" -> "deleteUsernameExistsMessage";
|
||||
default -> null;
|
||||
};
|
||||
|
||||
model.addAttribute("deleteMessage", deleteMessage);
|
||||
String addMessage = null;
|
||||
switch (messageType) {
|
||||
case "usernameExists":
|
||||
addMessage = "usernameExistsMessage";
|
||||
break;
|
||||
case "invalidUsername":
|
||||
addMessage = "invalidUsernameMessage";
|
||||
break;
|
||||
case "invalidPassword":
|
||||
addMessage = "invalidPasswordMessage";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
String addMessage;
|
||||
addMessage =
|
||||
switch (messageType) {
|
||||
case "usernameExists" -> "usernameExistsMessage";
|
||||
case "invalidUsername" -> "invalidUsernameMessage";
|
||||
case "invalidPassword" -> "invalidPasswordMessage";
|
||||
default -> null;
|
||||
};
|
||||
model.addAttribute("addMessage", addMessage);
|
||||
}
|
||||
String changeMessage = null;
|
||||
|
||||
String changeMessage;
|
||||
if (messageType != null) {
|
||||
switch (messageType) {
|
||||
case "userNotFound":
|
||||
changeMessage = "userNotFoundMessage";
|
||||
break;
|
||||
case "downgradeCurrentUser":
|
||||
changeMessage = "downgradeCurrentUserMessage";
|
||||
break;
|
||||
case "disabledCurrentUser":
|
||||
changeMessage = "disabledCurrentUserMessage";
|
||||
break;
|
||||
default:
|
||||
changeMessage = messageType;
|
||||
break;
|
||||
}
|
||||
changeMessage =
|
||||
switch (messageType) {
|
||||
case "userNotFound" -> "userNotFoundMessage";
|
||||
case "downgradeCurrentUser" -> "downgradeCurrentUserMessage";
|
||||
case "disabledCurrentUser" -> "disabledCurrentUserMessage";
|
||||
default -> messageType;
|
||||
};
|
||||
model.addAttribute("changeMessage", changeMessage);
|
||||
}
|
||||
|
||||
model.addAttribute("users", sortedUsers);
|
||||
model.addAttribute("currentUsername", authentication.getName());
|
||||
model.addAttribute("roleDetails", roleDetails);
|
||||
@@ -337,81 +327,54 @@ 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;
|
||||
if (principal instanceof UserDetails) {
|
||||
// Cast the principal object to UserDetails
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
// Retrieve username and other attributes
|
||||
username = userDetails.getUsername();
|
||||
// Add oAuth2 Login attributes to the model
|
||||
|
||||
// Retrieve username and other attributes and add login attributes to the model
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
model.addAttribute("oAuth2Login", false);
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
// Cast the principal object to OAuth2User
|
||||
OAuth2User userDetails = (OAuth2User) principal;
|
||||
// Retrieve username and other attributes
|
||||
username =
|
||||
userDetails.getAttribute(
|
||||
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||
// Add oAuth2 Login attributes to the model
|
||||
if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
model.addAttribute("oAuth2Login", true);
|
||||
}
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
// Cast the principal object to OAuth2User
|
||||
CustomSaml2AuthenticatedPrincipal userDetails =
|
||||
(CustomSaml2AuthenticatedPrincipal) principal;
|
||||
// Retrieve username and other attributes
|
||||
username = userDetails.getName();
|
||||
// Add oAuth2 Login attributes to the model
|
||||
model.addAttribute("oAuth2Login", true);
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
username = saml2User.name();
|
||||
model.addAttribute("saml2Login", true);
|
||||
}
|
||||
if (username != null) {
|
||||
// Fetch user details from the database
|
||||
Optional<User> user =
|
||||
userRepository
|
||||
.findByUsernameIgnoreCaseWithSettings( // Assuming findByUsername
|
||||
// method exists
|
||||
username);
|
||||
if (!user.isPresent()) {
|
||||
Optional<User> user = userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
||||
|
||||
if (user.isEmpty()) {
|
||||
return "redirect:/error";
|
||||
}
|
||||
|
||||
// Convert settings map to JSON string
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String settingsJson;
|
||||
try {
|
||||
settingsJson = objectMapper.writeValueAsString(user.get().getSettings());
|
||||
} catch (JsonProcessingException e) {
|
||||
// Handle JSON conversion error
|
||||
log.error("exception", e);
|
||||
log.error("Error converting settings map", e);
|
||||
return "redirect:/error";
|
||||
}
|
||||
|
||||
String messageType = request.getParameter("messageType");
|
||||
if (messageType != null) {
|
||||
switch (messageType) {
|
||||
case "notAuthenticated":
|
||||
messageType = "notAuthenticatedMessage";
|
||||
break;
|
||||
case "userNotFound":
|
||||
messageType = "userNotFoundMessage";
|
||||
break;
|
||||
case "incorrectPassword":
|
||||
messageType = "incorrectPasswordMessage";
|
||||
break;
|
||||
case "usernameExists":
|
||||
messageType = "usernameExistsMessage";
|
||||
break;
|
||||
case "invalidUsername":
|
||||
messageType = "invalidUsernameMessage";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case "notAuthenticated" -> messageType = "notAuthenticatedMessage";
|
||||
case "userNotFound" -> messageType = "userNotFoundMessage";
|
||||
case "incorrectPassword" -> messageType = "incorrectPasswordMessage";
|
||||
case "usernameExists" -> messageType = "usernameExistsMessage";
|
||||
case "invalidUsername" -> messageType = "invalidUsernameMessage";
|
||||
}
|
||||
model.addAttribute("messageType", messageType);
|
||||
}
|
||||
// Add attributes to the model
|
||||
|
||||
model.addAttribute("username", username);
|
||||
model.addAttribute("messageType", messageType);
|
||||
model.addAttribute("role", user.get().getRolesAsString());
|
||||
model.addAttribute("settings", settingsJson);
|
||||
model.addAttribute("changeCredsFlag", user.get().isFirstLogin());
|
||||
@@ -430,21 +393,14 @@ 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) {
|
||||
// Cast the principal object to UserDetails
|
||||
UserDetails userDetails = (UserDetails) principal;
|
||||
// Retrieve username and other attributes
|
||||
String username = userDetails.getUsername();
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
String username = detailsUser.getUsername();
|
||||
// Fetch user details from the database
|
||||
Optional<User> user =
|
||||
userRepository
|
||||
.findByUsernameIgnoreCase( // Assuming findByUsername method exists
|
||||
username);
|
||||
if (!user.isPresent()) {
|
||||
// Handle error appropriately
|
||||
// Example redirection in case of error
|
||||
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
|
||||
if (user.isEmpty()) {
|
||||
// Handle error appropriately, example redirection in case of error
|
||||
return "redirect:/error";
|
||||
}
|
||||
String messageType = request.getParameter("messageType");
|
||||
@@ -467,7 +423,7 @@ public class AccountWebController {
|
||||
}
|
||||
model.addAttribute("messageType", messageType);
|
||||
}
|
||||
// Add attributes to the model
|
||||
|
||||
model.addAttribute("username", username);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import static stirling.software.SPDF.utils.validation.Validator.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -12,7 +14,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -34,10 +35,11 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.InstallationPathConfig;
|
||||
import stirling.software.SPDF.config.YamlPropertySourceFactory;
|
||||
import stirling.software.SPDF.model.provider.GithubProvider;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.provider.GitHubProvider;
|
||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.model.provider.Provider;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "")
|
||||
@@ -136,13 +138,13 @@ public class ApplicationProperties {
|
||||
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
|
||||
}
|
||||
|
||||
public boolean isOauth2Activ() {
|
||||
public boolean isOauth2Active() {
|
||||
return (oauth2 != null
|
||||
&& oauth2.getEnabled()
|
||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||
}
|
||||
|
||||
public boolean isSaml2Activ() {
|
||||
public boolean isSaml2Active() {
|
||||
return (saml2 != null
|
||||
&& saml2.getEnabled()
|
||||
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
|
||||
@@ -158,6 +160,7 @@ public class ApplicationProperties {
|
||||
@Setter
|
||||
@ToString
|
||||
public static class SAML2 {
|
||||
private String provider;
|
||||
private Boolean enabled = false;
|
||||
private Boolean autoCreateUser = false;
|
||||
private Boolean blockRegistration = false;
|
||||
@@ -195,7 +198,7 @@ public class ApplicationProperties {
|
||||
}
|
||||
}
|
||||
|
||||
public Resource getidpCert() {
|
||||
public Resource getIdpCert() {
|
||||
if (idpCert == null) return null;
|
||||
if (idpCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||
@@ -225,12 +228,11 @@ public class ApplicationProperties {
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String provider;
|
||||
private Client client = new Client();
|
||||
private String logoutUrl;
|
||||
|
||||
public void setScopes(String scopes) {
|
||||
List<String> scopesList =
|
||||
Arrays.stream(scopes.split(","))
|
||||
.map(String::trim)
|
||||
.collect(Collectors.toList());
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).toList();
|
||||
this.scopes.addAll(scopesList);
|
||||
}
|
||||
|
||||
@@ -243,32 +245,31 @@ public class ApplicationProperties {
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return isValid(this.getIssuer(), "issuer")
|
||||
&& isValid(this.getClientId(), "clientId")
|
||||
&& isValid(this.getClientSecret(), "clientSecret")
|
||||
&& isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
return !isStringEmpty(this.getIssuer())
|
||||
&& !isStringEmpty(this.getClientId())
|
||||
&& !isStringEmpty(this.getClientSecret())
|
||||
&& !isCollectionEmpty(this.getScopes())
|
||||
&& !isStringEmpty(this.getUseAsUsername());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Client {
|
||||
private GoogleProvider google = new GoogleProvider();
|
||||
private GithubProvider github = new GithubProvider();
|
||||
private GitHubProvider github = new GitHubProvider();
|
||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||
|
||||
public Provider get(String registrationId) throws UnsupportedProviderException {
|
||||
switch (registrationId.toLowerCase()) {
|
||||
case "google":
|
||||
return getGoogle();
|
||||
case "github":
|
||||
return getGithub();
|
||||
case "keycloak":
|
||||
return getKeycloak();
|
||||
default:
|
||||
throw new UnsupportedProviderException(
|
||||
"Logout from the provider is not supported? Report it at"
|
||||
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||
}
|
||||
return switch (registrationId.toLowerCase()) {
|
||||
case "google" -> getGoogle();
|
||||
case "github" -> getGithub();
|
||||
case "keycloak" -> getKeycloak();
|
||||
default ->
|
||||
throw new UnsupportedProviderException(
|
||||
"Logout from the provider "
|
||||
+ registrationId
|
||||
+ " is not supported. "
|
||||
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,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
|
||||
@@ -335,10 +340,10 @@ public class ApplicationProperties {
|
||||
@Override
|
||||
public String toString() {
|
||||
return """
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
.formatted(driverName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class Provider implements ProviderInterface {
|
||||
private String name;
|
||||
private String clientName;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return clientName;
|
||||
}
|
||||
|
||||
protected boolean isValid(String value, String name) {
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean isValid(Collection<String> value, String name) {
|
||||
if (value != null && !value.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScopes(String scopes) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUseAsUsername() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSecret(String clientSecret) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientId(String clientId) {
|
||||
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface ProviderInterface {
|
||||
|
||||
public Collection<String> getScopes();
|
||||
|
||||
public void setScopes(String scopes);
|
||||
|
||||
public String getUseAsUsername();
|
||||
|
||||
public void setUseAsUsername(String useAsUsername);
|
||||
|
||||
public String getIssuer();
|
||||
|
||||
public void setIssuer(String issuer);
|
||||
|
||||
public String getClientSecret();
|
||||
|
||||
public void setClientSecret(String clientSecret);
|
||||
|
||||
public String getClientId();
|
||||
|
||||
public void setClientId(String clientId);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package stirling.software.SPDF.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum UsernameAttribute {
|
||||
EMAIL("email"),
|
||||
LOGIN("login"),
|
||||
PROFILE("profile"),
|
||||
NAME("name"),
|
||||
USERNAME("username"),
|
||||
NICKNAME("nickname"),
|
||||
GIVEN_NAME("given_name"),
|
||||
MIDDLE_NAME("middle_name"),
|
||||
FAMILY_NAME("family_name"),
|
||||
PREFERRED_NAME("preferred_name"),
|
||||
PREFERRED_USERNAME("preferred_username");
|
||||
|
||||
private final String name;
|
||||
|
||||
UsernameAttribute(final String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package stirling.software.SPDF.model.exception;
|
||||
|
||||
public class NoProviderFoundException extends Exception {
|
||||
public NoProviderFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public NoProviderFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
package stirling.software.SPDF.model.exception;
|
||||
|
||||
public class UnsupportedProviderException extends Exception {
|
||||
public UnsupportedProviderException(String message) {
|
||||
@@ -0,0 +1,7 @@
|
||||
package stirling.software.SPDF.model.exception;
|
||||
|
||||
public class UnsupportedUsernameAttribute extends RuntimeException {
|
||||
public UnsupportedUsernameAttribute(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class GitHubProvider extends Provider {
|
||||
|
||||
private static final String NAME = "github";
|
||||
private static final String CLIENT_NAME = "GitHub";
|
||||
private static final String AUTHORIZATION_URI = "https://github.com/login/oauth/authorize";
|
||||
private static final String TOKEN_URI = "https://github.com/login/oauth/access_token";
|
||||
private static final String USER_INFO_URI = "https://api.github.com/user";
|
||||
|
||||
public GitHubProvider(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
null,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername != null ? useAsUsername : UsernameAttribute.LOGIN,
|
||||
null,
|
||||
AUTHORIZATION_URI,
|
||||
TOKEN_URI,
|
||||
USER_INFO_URI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthorizationUri() {
|
||||
return AUTHORIZATION_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenUri() {
|
||||
return TOKEN_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserInfoUri() {
|
||||
return USER_INFO_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("read:user");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GitHub [clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import stirling.software.SPDF.model.Provider;
|
||||
|
||||
public class GithubProvider extends Provider {
|
||||
|
||||
private static final String authorizationUri = "https://github.com/login/oauth/authorize";
|
||||
private static final String tokenUri = "https://github.com/login/oauth/access_token";
|
||||
private static final String userInfoUri = "https://api.github.com/user";
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "login";
|
||||
|
||||
public String getAuthorizationuri() {
|
||||
return authorizationUri;
|
||||
}
|
||||
|
||||
public String getTokenuri() {
|
||||
return tokenUri;
|
||||
}
|
||||
|
||||
public String getUserinfouri() {
|
||||
return userInfoUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return new String();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("read:user");
|
||||
}
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScopes(String scopes) {
|
||||
this.scopes =
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUseAsUsername() {
|
||||
return this.useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
this.useAsUsername = useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GitHub [clientId="
|
||||
+ clientId
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "github";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "GitHub";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return super.isValid(this.getClientId(), "clientId")
|
||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||
&& super.isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
}
|
||||
}
|
||||
@@ -1,116 +1,86 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import stirling.software.SPDF.model.Provider;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class GoogleProvider extends Provider {
|
||||
|
||||
private static final String authorizationUri = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
|
||||
private static final String userInfoUri =
|
||||
private static final String NAME = "google";
|
||||
private static final String CLIENT_NAME = "Google";
|
||||
private static final String AUTHORIZATION_URI = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
private static final String TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token";
|
||||
private static final String USER_INFO_URI =
|
||||
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "email";
|
||||
|
||||
public String getAuthorizationuri() {
|
||||
return authorizationUri;
|
||||
public GoogleProvider(
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
null,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername,
|
||||
null,
|
||||
AUTHORIZATION_URI,
|
||||
TOKEN_URI,
|
||||
USER_INFO_URI);
|
||||
}
|
||||
|
||||
public String getTokenuri() {
|
||||
return tokenUri;
|
||||
public String getAuthorizationUri() {
|
||||
return AUTHORIZATION_URI;
|
||||
}
|
||||
|
||||
public String getUserinfouri() {
|
||||
return userInfoUri;
|
||||
public String getTokenUri() {
|
||||
return TOKEN_URI;
|
||||
}
|
||||
|
||||
public String getUserinfoUri() {
|
||||
return USER_INFO_URI;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return new String();
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.email");
|
||||
scopes.add("https://www.googleapis.com/auth/userinfo.profile");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScopes(String scopes) {
|
||||
this.scopes =
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUseAsUsername() {
|
||||
return this.useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
this.useAsUsername = useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Google [clientId="
|
||||
+ clientId
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "google";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "Google";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return super.isValid(this.getClientId(), "clientId")
|
||||
&& super.isValid(this.getClientSecret(), "clientSecret")
|
||||
&& super.isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,106 +1,73 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import stirling.software.SPDF.model.Provider;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
|
||||
@NoArgsConstructor
|
||||
public class KeycloakProvider extends Provider {
|
||||
|
||||
private String issuer;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes = new ArrayList<>();
|
||||
private String useAsUsername = "email";
|
||||
private static final String NAME = "keycloak";
|
||||
private static final String CLIENT_NAME = "Keycloak";
|
||||
|
||||
@Override
|
||||
public String getIssuer() {
|
||||
return this.issuer;
|
||||
public KeycloakProvider(
|
||||
String issuer,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername) {
|
||||
super(
|
||||
issuer,
|
||||
NAME,
|
||||
CLIENT_NAME,
|
||||
clientId,
|
||||
clientSecret,
|
||||
scopes,
|
||||
useAsUsername,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIssuer(String issuer) {
|
||||
this.issuer = issuer;
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
public String getClientName() {
|
||||
return CLIENT_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getScopes() {
|
||||
Collection<String> scopes = super.getScopes();
|
||||
|
||||
if (scopes == null || scopes.isEmpty()) {
|
||||
scopes = new ArrayList<>();
|
||||
scopes.add("profile");
|
||||
scopes.add("email");
|
||||
}
|
||||
|
||||
return scopes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setScopes(String scopes) {
|
||||
this.scopes =
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUseAsUsername() {
|
||||
return this.useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseAsUsername(String useAsUsername) {
|
||||
this.useAsUsername = useAsUsername;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Keycloak [issuer="
|
||||
+ issuer
|
||||
+ getIssuer()
|
||||
+ ", clientId="
|
||||
+ clientId
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (clientSecret != null && !clientSecret.isEmpty() ? "MASKED" : "NULL")
|
||||
+ (getClientSecret() != null && !getClientSecret().isBlank() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ scopes
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ useAsUsername
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "keycloak";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientName() {
|
||||
return "Keycloak";
|
||||
}
|
||||
|
||||
public boolean isSettingsValid() {
|
||||
return isValid(this.getIssuer(), "issuer")
|
||||
&& isValid(this.getClientId(), "clientId")
|
||||
&& isValid(this.getClientSecret(), "clientSecret")
|
||||
&& isValid(this.getScopes(), "scopes")
|
||||
&& isValid(this.getUseAsUsername(), "useAsUsername");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package stirling.software.SPDF.model.provider;
|
||||
|
||||
import static stirling.software.SPDF.model.UsernameAttribute.EMAIL;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.UsernameAttribute;
|
||||
import stirling.software.SPDF.model.exception.UnsupportedUsernameAttribute;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Provider {
|
||||
|
||||
public static final String EXCEPTION_MESSAGE = "The attribute %s is not supported for %s.";
|
||||
|
||||
private String issuer;
|
||||
private String name;
|
||||
private String clientName;
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private Collection<String> scopes;
|
||||
private UsernameAttribute useAsUsername;
|
||||
private String logoutUrl;
|
||||
private String authorizationUri;
|
||||
private String tokenUri;
|
||||
private String userInfoUri;
|
||||
|
||||
public Provider(
|
||||
String issuer,
|
||||
String name,
|
||||
String clientName,
|
||||
String clientId,
|
||||
String clientSecret,
|
||||
Collection<String> scopes,
|
||||
UsernameAttribute useAsUsername,
|
||||
String logoutUrl,
|
||||
String authorizationUri,
|
||||
String tokenUri,
|
||||
String userInfoUri) {
|
||||
this.issuer = issuer;
|
||||
this.name = name;
|
||||
this.clientName = clientName;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.scopes = scopes == null ? new ArrayList<>() : scopes;
|
||||
this.useAsUsername =
|
||||
useAsUsername != null ? validateUsernameAttribute(useAsUsername) : EMAIL;
|
||||
this.logoutUrl = logoutUrl;
|
||||
this.authorizationUri = authorizationUri;
|
||||
this.tokenUri = tokenUri;
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
|
||||
public void setScopes(String scopes) {
|
||||
if (scopes != null && !scopes.isBlank()) {
|
||||
this.scopes =
|
||||
Arrays.stream(scopes.split(",")).map(String::trim).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateUsernameAttribute(UsernameAttribute usernameAttribute) {
|
||||
switch (name) {
|
||||
case "google" -> {
|
||||
return validateGoogleUsernameAttribute(usernameAttribute);
|
||||
}
|
||||
case "github" -> {
|
||||
return validateGitHubUsernameAttribute(usernameAttribute);
|
||||
}
|
||||
case "keycloak" -> {
|
||||
return validateKeycloakUsernameAttribute(usernameAttribute);
|
||||
}
|
||||
default -> {
|
||||
return usernameAttribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateKeycloakUsernameAttribute(
|
||||
UsernameAttribute usernameAttribute) {
|
||||
switch (usernameAttribute) {
|
||||
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME, PREFERRED_USERNAME -> {
|
||||
return usernameAttribute;
|
||||
}
|
||||
default ->
|
||||
throw new UnsupportedUsernameAttribute(
|
||||
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateGoogleUsernameAttribute(UsernameAttribute usernameAttribute) {
|
||||
switch (usernameAttribute) {
|
||||
case EMAIL, NAME, GIVEN_NAME, FAMILY_NAME -> {
|
||||
return usernameAttribute;
|
||||
}
|
||||
default ->
|
||||
throw new UnsupportedUsernameAttribute(
|
||||
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
|
||||
}
|
||||
}
|
||||
|
||||
private UsernameAttribute validateGitHubUsernameAttribute(UsernameAttribute usernameAttribute) {
|
||||
switch (usernameAttribute) {
|
||||
case LOGIN, EMAIL, NAME -> {
|
||||
return usernameAttribute;
|
||||
}
|
||||
default ->
|
||||
throw new UnsupportedUsernameAttribute(
|
||||
String.format(EXCEPTION_MESSAGE, usernameAttribute, clientName));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Provider [name="
|
||||
+ getName()
|
||||
+ ", clientName="
|
||||
+ getClientName()
|
||||
+ ", clientId="
|
||||
+ getClientId()
|
||||
+ ", clientSecret="
|
||||
+ (getClientSecret() != null && !getClientSecret().isEmpty() ? "*****" : "NULL")
|
||||
+ ", scopes="
|
||||
+ getScopes()
|
||||
+ ", useAsUsername="
|
||||
+ getUseAsUsername()
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -84,8 +84,8 @@ public class CertificateValidationService {
|
||||
Enumeration<String> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Object> 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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -169,7 +169,7 @@ public class FileToPdf {
|
||||
}
|
||||
}
|
||||
|
||||
// search for the main HTML file.
|
||||
// Search for the main HTML file.
|
||||
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
||||
List<Path> 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<String> command = new ArrayList<>();
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path tempInputFile = null;
|
||||
|
||||
try {
|
||||
// Create temp file with appropriate extension
|
||||
tempInputFile = Files.createTempFile("input_", fileExtension);
|
||||
Files.write(tempInputFile, bytes);
|
||||
|
||||
command.add("ebook-convert");
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
return Files.readAllBytes(tempOutputFile);
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
if (tempInputFile != null) {
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
|
||||
static String sanitizeZipFilename(String entryName) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> lines = Files.readAllLines(settingsPath);
|
||||
|
||||
// 2) Build a map of "nestedKeyPath -> lineIndex" by parsing indentation
|
||||
// Also track each line's indentation so we can preserve it when rewriting.
|
||||
Map<String, LineInfo> pathToLine = parseNestedYamlKeys(lines);
|
||||
|
||||
// 3) If the path is found, rewrite its line. Else, append at the bottom (no indentation).
|
||||
boolean changed = false;
|
||||
if (pathToLine.containsKey(fullPath)) {
|
||||
// Rewrite existing line
|
||||
LineInfo info = pathToLine.get(fullPath);
|
||||
String oldLine = lines.get(info.lineIndex);
|
||||
String newLine =
|
||||
rewriteLine(oldLine, info.indentSpaces, fullPath, newValue, autoGenerated);
|
||||
if (!newLine.equals(oldLine)) {
|
||||
lines.set(info.lineIndex, newLine);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
// Append a new line at the bottom, with zero indentation
|
||||
String appended = fullPath + ": " + newValue;
|
||||
if (autoGenerated) {
|
||||
appended += " # Automatically Generated Settings (Do Not Edit Directly)";
|
||||
}
|
||||
lines.add(appended);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 4) If changed, write back to file
|
||||
if (changed) {
|
||||
Files.write(settingsPath, lines);
|
||||
log.info(
|
||||
"Updated '{}' to '{}' (autoGenerated={}) in {}",
|
||||
fullPath,
|
||||
newValue,
|
||||
autoGenerated,
|
||||
settingsPath);
|
||||
} else {
|
||||
log.info("No changes for '{}' (already set to '{}').", fullPath, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/** A small record-like class that holds: - lineIndex - indentSpaces */
|
||||
private static class LineInfo {
|
||||
int lineIndex;
|
||||
int indentSpaces;
|
||||
|
||||
public LineInfo(int lineIndex, int indentSpaces) {
|
||||
this.lineIndex = lineIndex;
|
||||
this.indentSpaces = indentSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the YAML lines to build a map: "full.nested.key" -> (lineIndex, indentSpaces). We do a
|
||||
* naive indentation-based path stacking: - 2 spaces = 1 indent level - lines that start with
|
||||
* fewer or equal indentation pop the stack - lines that look like "key:" or "key: value" cause
|
||||
* a push
|
||||
*/
|
||||
private static Map<String, LineInfo> parseNestedYamlKeys(List<String> lines) {
|
||||
Map<String, LineInfo> result = new HashMap<>();
|
||||
|
||||
// We'll maintain a stack of (keyName, indentLevel).
|
||||
// Each line that looks like "myKey:" or "myKey: value" is a new "child" of the top of the
|
||||
// stack if indent is deeper.
|
||||
Deque<String> pathStack = new ArrayDeque<>();
|
||||
Deque<Integer> indentStack = new ArrayDeque<>();
|
||||
indentStack.push(-1); // sentinel
|
||||
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String line = lines.get(i);
|
||||
String trimmed = line.trim();
|
||||
|
||||
// skip blank lines, comment lines, or list items
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
// check if there's a colon
|
||||
int colonIdx = trimmed.indexOf(':');
|
||||
if (colonIdx <= 0) { // must have at least one char before ':'
|
||||
continue;
|
||||
}
|
||||
// parse out key
|
||||
String keyPart = trimmed.substring(0, colonIdx).trim();
|
||||
if (keyPart.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// count leading spaces for indentation
|
||||
int leadingSpaces = countLeadingSpaces(line);
|
||||
int indentLevel = leadingSpaces / 2; // assume 2 spaces per level
|
||||
|
||||
// pop from stack until we get to a shallower indentation
|
||||
while (indentStack.peek() != null && indentStack.peek() >= indentLevel) {
|
||||
indentStack.pop();
|
||||
pathStack.pop();
|
||||
}
|
||||
|
||||
// push the new key
|
||||
pathStack.push(keyPart);
|
||||
indentStack.push(indentLevel);
|
||||
|
||||
// build the full path
|
||||
String[] arr = pathStack.toArray(new String[0]);
|
||||
List<String> reversed = Arrays.asList(arr);
|
||||
Collections.reverse(reversed);
|
||||
String fullPath = String.join(".", reversed);
|
||||
|
||||
// store line info
|
||||
result.put(fullPath, new LineInfo(i, leadingSpaces));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a single line to set a new value, preserving indentation and (optionally) the
|
||||
* existing or auto-generated inline comment.
|
||||
*
|
||||
* <p>For example, oldLine might be: " csrfDisabled: false # set to 'true' to disable CSRF
|
||||
* protection" newValue = "true" autoGenerated = false
|
||||
*
|
||||
* <p>We'll produce something like: " csrfDisabled: true # set to 'true' to disable CSRF
|
||||
* protection"
|
||||
*/
|
||||
private static String rewriteLine(
|
||||
String oldLine, int indentSpaces, String path, String newValue, boolean autoGenerated) {
|
||||
// We'll keep the exact leading indentation (indentSpaces).
|
||||
// Then "key: newValue". We'll try to preserve any existing inline comment unless
|
||||
// autoGenerated is true.
|
||||
|
||||
// 1) Extract leading spaces from the old line (just in case they differ from indentSpaces).
|
||||
int actualLeadingSpaces = countLeadingSpaces(oldLine);
|
||||
String leading = oldLine.substring(0, actualLeadingSpaces);
|
||||
|
||||
// 2) Remove leading spaces from the rest
|
||||
String trimmed = oldLine.substring(actualLeadingSpaces);
|
||||
|
||||
// 3) Check for existing comment
|
||||
int hashIndex = trimmed.indexOf('#');
|
||||
String lineWithoutComment =
|
||||
(hashIndex >= 0) ? trimmed.substring(0, hashIndex).trim() : trimmed.trim();
|
||||
String oldComment = (hashIndex >= 0) ? trimmed.substring(hashIndex).trim() : "";
|
||||
|
||||
// 4) Rebuild "key: newValue"
|
||||
// The "key" here is everything before ':' in lineWithoutComment
|
||||
int colonIdx = lineWithoutComment.indexOf(':');
|
||||
String existingKey =
|
||||
(colonIdx >= 0)
|
||||
? lineWithoutComment.substring(0, colonIdx).trim()
|
||||
: path; // fallback if line is malformed
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(leading); // restore original leading spaces
|
||||
|
||||
// "key: newValue"
|
||||
sb.append(existingKey).append(": ").append(newValue);
|
||||
|
||||
// 5) If autoGenerated, add/replace comment
|
||||
if (autoGenerated) {
|
||||
sb.append(" # Automatically Generated Settings (Do Not Edit Directly)");
|
||||
} else {
|
||||
// preserve the old comment if it exists
|
||||
if (!oldComment.isEmpty()) {
|
||||
sb.append(" ").append(oldComment);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static int countLeadingSpaces(String line) {
|
||||
int count = 0;
|
||||
for (char c : line.toCharArray()) {
|
||||
if (c == ' ') count++;
|
||||
else break;
|
||||
}
|
||||
return count;
|
||||
YamlHelper settingsYaml = new YamlHelper(settingsPath);
|
||||
settingsYaml.updateValue(Arrays.asList(keyArray), newValue);
|
||||
settingsYaml.saveOverride(settingsPath);
|
||||
}
|
||||
|
||||
public static String generateMachineFingerprint() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package stirling.software.SPDF.utils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public class UrlUtils {
|
||||
|
||||
private UrlUtils() {}
|
||||
|
||||
public static String getOrigin(HttpServletRequest request) {
|
||||
String scheme = request.getScheme(); // http or https
|
||||
String serverName = request.getServerName(); // localhost
|
||||
@@ -14,4 +15,20 @@ public class UrlUtils {
|
||||
|
||||
return scheme + "://" + serverName + ":" + serverPort + contextPath;
|
||||
}
|
||||
|
||||
public static boolean isPortAvailable(int port) {
|
||||
try (ServerSocket socket = new ServerSocket(port)) {
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static String findAvailablePort(int startPort) {
|
||||
int port = startPort;
|
||||
while (!isPortAvailable(port)) {
|
||||
port++;
|
||||
}
|
||||
return String.valueOf(port);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package stirling.software.SPDF.utils.validation;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import stirling.software.SPDF.model.provider.Provider;
|
||||
|
||||
public class Validator {
|
||||
|
||||
public static boolean validateProvider(Provider provider) {
|
||||
if (provider == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isStringEmpty(provider.getClientId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isStringEmpty(provider.getClientSecret())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCollectionEmpty(provider.getScopes())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isStringEmpty(String input) {
|
||||
return input == null || input.isBlank();
|
||||
}
|
||||
|
||||
public static boolean isCollectionEmpty(Collection<String> input) {
|
||||
return input == null || input.isEmpty();
|
||||
}
|
||||
}
|
||||
@@ -572,8 +572,8 @@ login.invalid=اسم المستخدم أو كلمة المرور غير صالح
|
||||
login.locked=تم قفل حسابك.
|
||||
login.signinTitle=الرجاء تسجيل الدخول
|
||||
login.ssoSignIn=تسجيل الدخول عبر تسجيل الدخول الأحادي
|
||||
login.oauth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2
|
||||
login.oauth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول.
|
||||
login.oAuth2AutoCreateDisabled=تم تعطيل الإنشاء التلقائي لمستخدم OAuth2
|
||||
login.oAuth2AdminBlockedUser=تم حظر تسجيل أو تسجيل دخول المستخدمين غير المسجلين حاليًا. يرجى الاتصال بالمسؤول.
|
||||
login.oauth2RequestNotFound=لم يتم العثور على طلب التفويض
|
||||
login.oauth2InvalidUserInfoResponse=استجابة معلومات المستخدم غير صالحة
|
||||
login.oauth2invalidRequest=طلب غير صالح
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=تحويل إلى PDF
|
||||
compress.title=ضغط
|
||||
compress.header=ضغط ملف PDF
|
||||
compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF.
|
||||
compress.grayscale.label=تطبيق التدرج الرمادي للضغط
|
||||
compress.selectText.1=الوضع اليدوي - من 1 إلى 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=مستوى التحسين:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=تطبيق التدرج الرمادي للضغط
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Etibarsız istifadəçi adı və ya şifr.
|
||||
login.locked=Sizin hesabınız kilidlənmişdir.
|
||||
login.signinTitle=Zəhmət olmasa, daxil olun
|
||||
login.ssoSignIn=Single Sign-on vasitəsilə daxil olun
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir
|
||||
login.oauth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create İstifadəçisi Deaktivləşdirilmişdir
|
||||
login.oAuth2AdminBlockedUser=Qeydiyyatdan keçməmiş istifadəçilərin qeydiyyatı və daxil olması hal-hazırda bloklanmışdır. Zəhmət olmasa, administratorla əlaqə saxlayın.
|
||||
login.oauth2RequestNotFound=Təsdiqlənmə sorğusu tapılmadı
|
||||
login.oauth2InvalidUserInfoResponse=Yanlış İstifadəçi Məlumatı Cavabı
|
||||
login.oauth2invalidRequest=Etibarsız Sorğu
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF-ə Çevir
|
||||
compress.title=Sıxışdır
|
||||
compress.header=PDF-i Sıxışdır
|
||||
compress.credit=Bu servis PDF sıxışdırılması/Optimizasiyası üçün Ghostscript istifadə edir.
|
||||
compress.grayscale.label=Sıxma üçün Boz Rəng Tətbiq Edin
|
||||
compress.selectText.1=Manual Mod - 1-dən 5-ə
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimizasiya səviyyəsi:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Sıxma üçün Boz Rəng Tətbiq Edin
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Невалидно потребителско име или пар
|
||||
login.locked=Вашият акаунт е заключен.
|
||||
login.signinTitle=Моля впишете се
|
||||
login.ssoSignIn=Влизане чрез еднократно влизане
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
|
||||
login.oauth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
|
||||
login.oAuth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
|
||||
login.oauth2RequestNotFound=Заявката за оторизация не е намерена
|
||||
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
|
||||
login.oauth2invalidRequest=Невалидна заявка
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Преобразуване към PDF
|
||||
compress.title=Компресиране
|
||||
compress.header=Компресиране на PDF
|
||||
compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране.
|
||||
compress.grayscale.label=Приложи сива скала за компресиране
|
||||
compress.selectText.1=Ръчен режим - от 1 до 5
|
||||
compress.selectText.1.1=При нива на оптимизация от 6 до 9, в допълнение към общото компресиране на PDF, резолюцията на изображението се намалява, за да се намали допълнително размерът на файла. По-високите нива водят до по-силна компресия на изображенията (до 50% от оригиналния размер), като се постига по-голямо намаляване на размера, но с потенциална загуба на качество на изображенията.
|
||||
compress.selectText.2=Ниво на оптимизация:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Версия
|
||||
validateSignature.cert.keyUsage=Предназначение на ключа за използване
|
||||
validateSignature.cert.selfSigned=Самостоятелно подписан
|
||||
validateSignature.cert.bits=битове
|
||||
compress.grayscale.label=Приложи сива скала за компресиране
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nom d'usuari/contrasenya no vàlid
|
||||
login.locked=Compte bloquejat
|
||||
login.signinTitle=Autenticat
|
||||
login.ssoSignIn=Inicia sessió mitjançant inici de sessió únic
|
||||
login.oauth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada
|
||||
login.oauth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador.
|
||||
login.oAuth2AutoCreateDisabled=La creació automàtica d'usuaris OAUTH2 està desactivada
|
||||
login.oAuth2AdminBlockedUser=El registre o inici de sessió d'usuaris no registrats està actualment bloquejat. Si us plau, contacta amb l'administrador.
|
||||
login.oauth2RequestNotFound=Sol·licitud d'autorització no trobada
|
||||
login.oauth2InvalidUserInfoResponse=Resposta d'informació d'usuari no vàlida
|
||||
login.oauth2invalidRequest=Sol·licitud no vàlida
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Converteix a PDF
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Aquest servei utilitza qpdf per a la compressió/optimització de PDF.
|
||||
compress.grayscale.label=Aplicar escala de grisos per a la compressió
|
||||
compress.selectText.1=Mode manual: de l'1 al 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Nivell d'optimització:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplicar escala de grisos per a la compressió
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Neplatné uživatelské jméno nebo heslo.
|
||||
login.locked=Váš účet byl uzamčen.
|
||||
login.signinTitle=Prosím přihlaste se
|
||||
login.ssoSignIn=Přihlásit se přes Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno
|
||||
login.oauth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce.
|
||||
login.oAuth2AutoCreateDisabled=Automatické vytváření OAUTH2 uživatelů je zakázáno
|
||||
login.oAuth2AdminBlockedUser=Registrace nebo přihlášení neregistrovaných uživatelů je momentálně blokováno. Kontaktujte prosím správce.
|
||||
login.oauth2RequestNotFound=Požadavek na autorizaci nebyl nalezen
|
||||
login.oauth2InvalidUserInfoResponse=Neplatná odpověď s informacemi o uživateli
|
||||
login.oauth2invalidRequest=Neplatný požadavek
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Převést na PDF
|
||||
compress.title=Komprimovat
|
||||
compress.header=Komprimovat PDF
|
||||
compress.credit=Tato služba používá qpdf pro kompresi/optimalizaci PDF.
|
||||
compress.grayscale.label=Použít stupnici šedi pro kompresi
|
||||
compress.selectText.1=Ruční režim - Od 1 do 5
|
||||
compress.selectText.1.1=V úrovních optimalizace 6 až 9 je kromě obecné komprese PDF sníženo rozlišení obrázků pro další zmenšení velikosti souboru. Vyšší úrovně vedou k silnější kompresi obrázků (až na 50 % původní velikosti), čímž dosahují většího zmenšení velikosti, ale s potenciální ztrátou kvality obrázků.
|
||||
compress.selectText.2=Úroveň optimalizace:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Verze
|
||||
validateSignature.cert.keyUsage=Použití klíče
|
||||
validateSignature.cert.selfSigned=Podepsaný sám sebou
|
||||
validateSignature.cert.bits=bitů
|
||||
compress.grayscale.label=Použít stupnici šedi pro kompresi
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Ugyldigt brugernavn eller adgangskode.
|
||||
login.locked=Din konto er blevet låst.
|
||||
login.signinTitle=Log venligst ind
|
||||
login.ssoSignIn=Log ind via Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret
|
||||
login.oauth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opret Bruger Deaktiveret
|
||||
login.oAuth2AdminBlockedUser=Registrering eller login af ikke-registrerede brugere er i øjeblikket blokeret. Kontakt venligst administratoren.
|
||||
login.oauth2RequestNotFound=Autorisationsanmodning ikke fundet
|
||||
login.oauth2InvalidUserInfoResponse=Ugyldigt Brugerinfo Svar
|
||||
login.oauth2invalidRequest=Ugyldig Anmodning
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertér til PDF
|
||||
compress.title=Komprimer
|
||||
compress.header=Komprimer PDF
|
||||
compress.credit=Denne tjeneste bruger qpdf til PDF Komprimering/Optimering.
|
||||
compress.grayscale.label=Anvend gråskala til komprimering
|
||||
compress.selectText.1=Manuel Tilstand - Fra 1 til 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimeringsniveau:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Anvend gråskala til komprimering
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Benutzername oder Passwort ungültig.
|
||||
login.locked=Ihr Konto wurde gesperrt.
|
||||
login.signinTitle=Bitte melden Sie sich an.
|
||||
login.ssoSignIn=Anmeldung per Single Sign-On
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
|
||||
login.oauth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Benutzer automatisch erstellen deaktiviert
|
||||
login.oAuth2AdminBlockedUser=Die Registrierung bzw. das anmelden von nicht registrierten Benutzern ist derzeit gesperrt. Bitte wenden Sie sich an den Administrator.
|
||||
login.oauth2RequestNotFound=Autorisierungsanfrage nicht gefunden
|
||||
login.oauth2InvalidUserInfoResponse=Ungültige Benutzerinformationsantwort
|
||||
login.oauth2invalidRequest=ungültige Anfrage
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=In PDF konvertieren
|
||||
compress.title=Komprimieren
|
||||
compress.header=PDF komprimieren
|
||||
compress.credit=Dieser Dienst verwendet qpdf für die PDF-Komprimierung/-Optimierung.
|
||||
compress.grayscale.label=Graustufen für Komprimierung anwenden
|
||||
compress.selectText.1=Manueller Modus – Von 1 bis 5
|
||||
compress.selectText.1.1=In den Optimierungsstufen 6 bis 9 wird zusätzlich zur allgemeinen PDF-Komprimierung die Bildauflösung reduziert, um die Dateigröße weiter zu verringern. Höhere Stufen führen zu einer stärkeren Bildkomprimierung (bis zu 50 % der Originalgröße), wodurch eine stärkere Größenreduzierung erreicht wird, die jedoch mit einem möglichen Qualitätsverlust der Bilder einhergeht.
|
||||
compress.selectText.2=Optimierungsstufe:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Schlüsselverwendung
|
||||
validateSignature.cert.selfSigned=Selbstsigniert
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Graustufen für Komprimierung anwenden
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Μη έγκυρο όνομα χρήστη ή κωδικός.
|
||||
login.locked=Ο λογαριασμός σας έχει κλειδωθεί.
|
||||
login.signinTitle=Παρακαλώ συνδεθείτε
|
||||
login.ssoSignIn=Σύνδεση μέσω Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη
|
||||
login.oauth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή.
|
||||
login.oAuth2AutoCreateDisabled=Η αυτόματη δημιουργία χρήστη OAUTH2 είναι απενεργοποιημένη
|
||||
login.oAuth2AdminBlockedUser=Η εγγραφή ή σύνδεση μη εγγεγραμμένων χρηστών είναι προς το παρόν αποκλεισμένη. Παρακαλώ επικοινωνήστε με τον διαχειριστή.
|
||||
login.oauth2RequestNotFound=Το αίτημα εξουσιοδότησης δεν βρέθηκε
|
||||
login.oauth2InvalidUserInfoResponse=Μη έγκυρη απόκριση πληροφοριών χρήστη
|
||||
login.oauth2invalidRequest=Μη έγκυρο αίτημα
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Μετατροπή σε PDF
|
||||
compress.title=Συμπίεση
|
||||
compress.header=Συμπίεση PDF
|
||||
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για συμπίεση/βελτιστοποίηση PDF.
|
||||
compress.grayscale.label=Εφαρμογή κλίμακας του γκρι για συμπίεση
|
||||
compress.selectText.1=Χειροκίνητη λειτουργία - Από 1 έως 5
|
||||
compress.selectText.1.1=Στα επίπεδα βελτιστοποίησης 6 έως 9, εκτός από τη γενική συμπίεση PDF, η ανάλυση εικόνας μειώνεται για περαιτέρω μείωση του μεγέθους αρχείου. Υψηλότερα επίπεδα οδηγούν σε ισχυρότερη συμπίεση εικόνας (έως και 50% του αρχικού μεγέθους), επιτυγχάνοντας μεγαλύτερη μείωση μεγέθους αλλά με πιθανή απώλεια ποιότητας στις εικόνες.
|
||||
compress.selectText.2=Επίπεδο βελτιστοποίησης:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Έκδοση
|
||||
validateSignature.cert.keyUsage=Χρήση κλειδιού
|
||||
validateSignature.cert.selfSigned=Αυτο-υπογεγραμμένο
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Εφαρμογή κλίμακας του γκρι για συμπίεση
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
|
||||
login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Login via Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Convert to PDF
|
||||
compress.title=Compress
|
||||
compress.header=Compress PDF
|
||||
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
|
||||
compress.grayscale.label=Apply Grayscale for Compression
|
||||
compress.selectText.1=Manual Mode - From 1 to 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimisation level:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Apply Grayscale for Compression
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Invalid username or password.
|
||||
login.locked=Your account has been locked.
|
||||
login.signinTitle=Please sign in
|
||||
login.ssoSignIn=Login via Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Create User Disabled
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Convert to PDF
|
||||
compress.title=Compress
|
||||
compress.header=Compress PDF
|
||||
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
|
||||
compress.grayscale.label=Apply Grayscale for Compression
|
||||
compress.selectText.1=Manual Mode - From 1 to 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimization level:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Apply Grayscale for Compression
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nombre de usuario o contraseña erróneos.
|
||||
login.locked=Su cuenta se ha bloqueado.
|
||||
login.signinTitle=Por favor, inicie sesión
|
||||
login.ssoSignIn=Iniciar sesión a través del inicio de sesión único
|
||||
login.oauth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
|
||||
login.oauth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador.
|
||||
login.oAuth2AutoCreateDisabled=Usuario de creación automática de OAUTH2 DESACTIVADO
|
||||
login.oAuth2AdminBlockedUser=El registro o inicio de sesión de usuarios no registrados está actualmente bloqueado. Por favor, contáctese con el administrador.
|
||||
login.oauth2RequestNotFound=Solicitud de autorización no encontrada
|
||||
login.oauth2InvalidUserInfoResponse=Respuesta de información de usuario no válida
|
||||
login.oauth2invalidRequest=Solicitud no válida
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertir a PDF
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Este servicio utiliza qpdf para compresión/optimización de PDF
|
||||
compress.grayscale.label=Aplicar escala de grises para compresión
|
||||
compress.selectText.1=Modo manual - De 1 a 5
|
||||
compress.selectText.1.1=En los niveles de optimización 6 a 9, además de la compresión general de PDF, se reduce la resolución de la imagen para reducir aún más el tamaño del archivo. Los niveles más altos dan como resultado una mayor compresión de la imagen (hasta el 50 % del tamaño original), lo que permite lograr una mayor reducción del tamaño, pero con una posible pérdida de calidad en las imágenes.
|
||||
compress.selectText.2=Nivel de optimización:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versión
|
||||
validateSignature.cert.keyUsage=Uso de la llave
|
||||
validateSignature.cert.selfSigned=Autofirmado
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplicar escala de grises para compresión
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Okerreko erabiltzaile izena edo pasahitza.
|
||||
login.locked=Zure kontua blokeatu egin da.
|
||||
login.signinTitle=Mesedez, hasi saioa
|
||||
login.ssoSignIn=Hasi saioa Saioa hasteko modu bakarraren bidez
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Sortu automatikoki erabiltzailea desgaituta dago
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF bihurtu
|
||||
compress.title=Konprimatu
|
||||
compress.header=PDFa konprimatu
|
||||
compress.credit=Zerbitzu honek qpdf erabiltzen du PDFak komprimatzeko/optimizatzeko
|
||||
compress.grayscale.label=Aplikatu grisezko eskala konpresiorako
|
||||
compress.selectText.1=Eskuz 1etik 5ra
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimizazio maila:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplikatu grisezko eskala konpresiorako
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=نام کاربری یا رمز عبور اشتباه است.
|
||||
login.locked=حساب شما قفل شده است.
|
||||
login.signinTitle=لطفاً وارد شوید
|
||||
login.ssoSignIn=ورود از طریق Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است
|
||||
login.oauth2AdminBlockedUser=ثبتنام یا ورود کاربران ثبتنشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید.
|
||||
login.oAuth2AutoCreateDisabled=ایجاد خودکار کاربر با OAUTH2 غیرفعال است
|
||||
login.oAuth2AdminBlockedUser=ثبتنام یا ورود کاربران ثبتنشده در حال حاضر مسدود است. لطفاً با مدیر تماس بگیرید.
|
||||
login.oauth2RequestNotFound=درخواست احراز هویت پیدا نشد
|
||||
login.oauth2InvalidUserInfoResponse=پاسخ اطلاعات کاربری نامعتبر است
|
||||
login.oauth2invalidRequest=درخواست نامعتبر
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=تبدیل به PDF
|
||||
compress.title=فشردهسازی
|
||||
compress.header=فشردهسازی PDF
|
||||
compress.credit=این سرویس از qpdf برای فشردهسازی / بهینهسازی PDF استفاده میکند.
|
||||
compress.grayscale.label=اعمال مقیاس خاکستری برای فشردهسازی
|
||||
compress.selectText.1=حالت دستی - از 1 تا 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=سطح بهینهسازی:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=نسخه
|
||||
validateSignature.cert.keyUsage=کاربرد کلید
|
||||
validateSignature.cert.selfSigned=با امضای خود
|
||||
validateSignature.cert.bits=بیتها
|
||||
compress.grayscale.label=اعمال مقیاس خاکستری برای فشردهسازی
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nom d'utilisateur ou mot de passe invalide.
|
||||
login.locked=Votre compte a été verrouillé.
|
||||
login.signinTitle=Veuillez vous connecter
|
||||
login.ssoSignIn=Se connecter via l'authentification unique
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
|
||||
login.oauth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Création automatique d'utilisateur désactivée
|
||||
login.oAuth2AdminBlockedUser=La création ou l'authentification d'utilisateurs non enregistrés est actuellement bloquée. Veuillez contacter l'administrateur.
|
||||
login.oauth2RequestNotFound=Demande d'autorisation introuvable
|
||||
login.oauth2InvalidUserInfoResponse=Réponse contenant les informations de l'utilisateur est invalide
|
||||
login.oauth2invalidRequest=Requête invalide
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertir
|
||||
compress.title=Compresser un PDF
|
||||
compress.header=Compresser un PDF (lorsque c'est possible!)
|
||||
compress.credit=Ce service utilise qpdf pour la compression et l'optimisation des PDF.
|
||||
compress.grayscale.label=Appliquer l'échelle de gris pour la compression
|
||||
compress.selectText.1=Mode manuel – de 1 à 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Niveau d'optimisation
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Usage de la clé
|
||||
validateSignature.cert.selfSigned=Auto-signé
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Appliquer l'échelle de gris pour la compression
|
||||
|
||||
@@ -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í
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Tiontaigh go PDF
|
||||
compress.title=Comhbhrúigh
|
||||
compress.header=Comhbhrúigh PDF
|
||||
compress.credit=Úsáideann an tseirbhís seo qpdf le haghaidh Comhbhrú/Optimization PDF.
|
||||
compress.grayscale.label=Cuir Scála Liath i bhFeidhm le Comhbhrú
|
||||
compress.selectText.1=Mód Láimhe - Ó 1 go 5
|
||||
compress.selectText.1.1=I leibhéil optamaithe 6 go 9, chomh maith le comhbhrú ginearálta PDF, déantar réiteach íomhá a laghdú de réir scála chun méid comhaid a laghdú tuilleadh. Mar thoradh ar leibhéil níos airde tá comhbhrú íomhá níos láidre (suas le 50% den mhéid bunaidh), ag baint amach laghdú méide níos mó ach le caillteanas cáilíochta féideartha in íomhánna.
|
||||
compress.selectText.2=Leibhéal optamaithe:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Leagan
|
||||
validateSignature.cert.keyUsage=Úsáid Eochrach
|
||||
validateSignature.cert.selfSigned=Féin-Sínithe
|
||||
validateSignature.cert.bits=giotáin
|
||||
compress.grayscale.label=Cuir Scála Liath i bhFeidhm le Comhbhrú
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=अमान्य उपयोगकर्ता नाम या
|
||||
login.locked=आपका खाता लॉक कर दिया गया है।
|
||||
login.signinTitle=कृपया साइन इन करें
|
||||
login.ssoSignIn=सिंगल साइन-ऑन के माध्यम से लॉगिन करें
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है
|
||||
login.oauth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें।
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 स्वतः उपयोगकर्ता निर्माण अक्षम है
|
||||
login.oAuth2AdminBlockedUser=गैर-पंजीकृत उपयोगकर्ताओं का पंजीकरण या लॉगिन वर्तमान में अवरुद्ध है। कृपया व्यवस्थापक से संपर्क करें।
|
||||
login.oauth2RequestNotFound=प्राधिकरण अनुरोध नहीं मिला
|
||||
login.oauth2InvalidUserInfoResponse=अमान्य उपयोगकर्ता जानकारी प्रतिक्रिया
|
||||
login.oauth2invalidRequest=अमान्य अनुरोध
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF में बदलें
|
||||
compress.title=कम्प्रेस
|
||||
compress.header=PDF कम्प्रेस करें
|
||||
compress.credit=यह सेवा PDF कम्प्रेस/अनुकूलन के लिए qpdf का उपयोग करती है।
|
||||
compress.grayscale.label=संपीड़न के लिए ग्रेस्केल लागू करें
|
||||
compress.selectText.1=मैनुअल मोड - स्तर 1 से 4
|
||||
compress.selectText.1.1=अनुकूलन स्तर 6 से 9 में, सामान्य PDF कम्प्रेसन के अतिरिक्त, फ़ाइल आकार को और कम करने के लिए छवि रेज़ोल्यूशन को कम किया जाता है। उच्च स्तर पर छवियों का अधिक कम्प्रेसन होता है (मूल आकार का 50% तक), जिससे आकार में अधिक कमी आती है लेकिन छवियों की गुणवत्ता प्रभावित हो सकती है।
|
||||
compress.selectText.2=अनुकूलन स्तर:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=संस्करण
|
||||
validateSignature.cert.keyUsage=कुंजी उपयोग
|
||||
validateSignature.cert.selfSigned=स्व-हस्ताक्षरित
|
||||
validateSignature.cert.bits=बिट्स
|
||||
compress.grayscale.label=संपीड़न के लिए ग्रेस्केल लागू करें
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili zaporka.
|
||||
login.locked=Vaš račun je zaključan.
|
||||
login.signinTitle=Molimo vas da se prijavite
|
||||
login.ssoSignIn=Prijavite se putem jedinstvene prijave
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
|
||||
login.oauth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
|
||||
login.oAuth2AdminBlockedUser=Registracija ili prijava nekadreguiranih korisnika trenutno su blokirane. Molimo Vas da kontaktirate administratora.
|
||||
login.oauth2RequestNotFound=Zahtjev za autorizaciju nije pronađen
|
||||
login.oauth2InvalidUserInfoResponse=Nevažeće informacije o korisniku
|
||||
login.oauth2invalidRequest=Neispravan zahtjev
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Pretvori u PDF
|
||||
compress.title=Komprimirajte
|
||||
compress.header=Komprimirajte PDF
|
||||
compress.credit=Ova usluga koristi qpdf za komprimiranje / optimizaciju PDF-a.
|
||||
compress.grayscale.label=Primijeni sivinu za kompresiju
|
||||
compress.selectText.1=Ručni režim - Od 1 do 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Nivo optimizacije:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Primijeni sivinu za kompresiju
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Érvénytelen felhasználónév vagy jelszó.
|
||||
login.locked=A fiókja zárolva van.
|
||||
login.signinTitle=Kérjük, jelentkezzen be
|
||||
login.ssoSignIn=Bejelentkezés egyszeri bejelentkezéssel
|
||||
login.oauth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva
|
||||
login.oauth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához.
|
||||
login.oAuth2AutoCreateDisabled=OAuth2 automatikus felhasználólétrehozás letiltva
|
||||
login.oAuth2AdminBlockedUser=A nem regisztrált felhasználók regisztrációja vagy bejelentkezése jelenleg le van tiltva. Kérjük, forduljon a rendszergazdához.
|
||||
login.oauth2RequestNotFound=A hitelesítési kérés nem található
|
||||
login.oauth2InvalidUserInfoResponse=Érvénytelen felhasználói információ válasz
|
||||
login.oauth2invalidRequest=Érvénytelen kérés
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertálás PDF-be
|
||||
compress.title=Tömörítés
|
||||
compress.header=PDF tömörítése
|
||||
compress.credit=Ez a szolgáltatás a qpdf használatával végzi a PDF tömörítését/optimalizálását.
|
||||
compress.grayscale.label=Szürkeárnyalatok alkalmazása tömörítéshez
|
||||
compress.selectText.1=Kézi mód - 1-től 5-ig
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimalizálási szint:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Verzió
|
||||
validateSignature.cert.keyUsage=Kulcshasználat
|
||||
validateSignature.cert.selfSigned=Önaláírt
|
||||
validateSignature.cert.bits=bit
|
||||
compress.grayscale.label=Szürkeárnyalatok alkalmazása tömörítéshez
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nama pengguna atau kata sandi tidak valid.
|
||||
login.locked=Akun Anda telah dikunci.
|
||||
login.signinTitle=Silakan masuk
|
||||
login.ssoSignIn=Masuk melalui Single Sign - on
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
|
||||
login.oauth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Buat Otomatis Pengguna Dinonaktifkan
|
||||
login.oAuth2AdminBlockedUser=Registrasi atau login pengguna yang tidak terdaftar saat ini diblokir. Silakan hubungi administrator.
|
||||
login.oauth2RequestNotFound=Permintaan otorisasi tidak ditemukan
|
||||
login.oauth2InvalidUserInfoResponse=Respons Info Pengguna Tidak Valid
|
||||
login.oauth2invalidRequest=Permintaan Tidak Valid
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konversi ke PDF
|
||||
compress.title=Kompres
|
||||
compress.header=Kompres PDF
|
||||
compress.credit=Layanan ini menggunakan qpdf untuk Kompresi/Optimalisasi PDF.
|
||||
compress.grayscale.label=Terapkan Skala Abu-Abu untuk Kompresi
|
||||
compress.selectText.1=Mode Manual - Dari 1 hingga 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Tingkat Optimalisasi:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Terapkan Skala Abu-Abu untuk Kompresi
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nome utente o password errati.
|
||||
login.locked=Il tuo account è stato bloccato.
|
||||
login.signinTitle=Per favore accedi
|
||||
login.ssoSignIn=Accedi tramite Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
|
||||
login.oauth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
|
||||
login.oAuth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
|
||||
login.oAuth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
|
||||
login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata
|
||||
login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
|
||||
login.oauth2invalidRequest=Richiesta non valida
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Converti in PDF
|
||||
compress.title=Comprimi
|
||||
compress.header=Comprimi PDF
|
||||
compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF.
|
||||
compress.grayscale.label=Applica scala di grigio per la compressione
|
||||
compress.selectText.1=Modalità manuale - Da 1 a 5
|
||||
compress.selectText.1.1=Nei livelli di ottimizzazione da 6 a 9, oltre alla compressione PDF generale, la risoluzione dell'immagine viene ridotta per ridurre ulteriormente le dimensioni del file. Livelli più alti comportano una compressione dell'immagine più forte (fino al 50% delle dimensioni originali), ottenendo una maggiore riduzione delle dimensioni ma con una potenziale perdita di qualità nelle immagini.
|
||||
compress.selectText.2=Livello di ottimizzazione:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versione
|
||||
validateSignature.cert.keyUsage=Utilizzo della chiave
|
||||
validateSignature.cert.selfSigned=Autofirmato
|
||||
validateSignature.cert.bits=bit
|
||||
compress.grayscale.label=Applica scala di grigio per la compressione
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=ユーザー名かパスワードが無効です。
|
||||
login.locked=あなたのアカウントはロックされています。
|
||||
login.signinTitle=サインインしてください
|
||||
login.ssoSignIn=シングルサインオンでログイン
|
||||
login.oauth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
|
||||
login.oauth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。
|
||||
login.oAuth2AutoCreateDisabled=OAuth 2自動作成ユーザーが無効
|
||||
login.oAuth2AdminBlockedUser=現在、未登録ユーザーの登録またはログインはブロックされています。管理者にお問い合わせください。
|
||||
login.oauth2RequestNotFound=認証リクエストが見つかりません
|
||||
login.oauth2InvalidUserInfoResponse=無効なユーザー情報の応答
|
||||
login.oauth2invalidRequest=無効なリクエスト
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=PDFを変換
|
||||
compress.title=圧縮
|
||||
compress.header=PDFを圧縮
|
||||
compress.credit=本サービスはPDFの圧縮/最適化にqpdfを使用しています。
|
||||
compress.grayscale.label=圧縮にグレースケールを適用する
|
||||
compress.selectText.1=手動モード - 1から9
|
||||
compress.selectText.1.1=最適化レベル6~9では、一般的なPDF圧縮に加えて画像解像度が縮小され、ファイルサイズがさらに縮小されます。レベルが高くなると、画像圧縮が強化され (元のサイズの最大 50%)、サイズはさらに縮小されますが、画像の品質が低下する可能性があります。
|
||||
compress.selectText.2=品質レベル:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=バージョン
|
||||
validateSignature.cert.keyUsage=キーの使用法
|
||||
validateSignature.cert.selfSigned=自己署名
|
||||
validateSignature.cert.bits=ビット
|
||||
compress.grayscale.label=圧縮にグレースケールを適用する
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=사용자 이름 또는 비밀번호가 잘못되었습니다.
|
||||
login.locked=계정이 잠겼습니다.
|
||||
login.signinTitle=로그인해 주세요
|
||||
login.ssoSignIn=단일 로그인으로 로그인
|
||||
login.oauth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다
|
||||
login.oauth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요.
|
||||
login.oAuth2AutoCreateDisabled=OAuth2 사용자 자동 생성이 비활성화되었습니다
|
||||
login.oAuth2AdminBlockedUser=현재 미등록 사용자의 등록 또는 로그인이 차단되어 있습니다. 관리자에게 문의하세요.
|
||||
login.oauth2RequestNotFound=인증 요청을 찾을 수 없습니다
|
||||
login.oauth2InvalidUserInfoResponse=잘못된 사용자 정보 응답
|
||||
login.oauth2invalidRequest=잘못된 요청
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=PDF로 변환
|
||||
compress.title=압축
|
||||
compress.header=PDF 압축
|
||||
compress.credit=이 서비스는 PDF 압축/최적화를 위해 qpdf를 사용합니다.
|
||||
compress.grayscale.label=압축을 위해 그레이스케일 적용
|
||||
compress.selectText.1=수동 모드 - 1에서 5
|
||||
compress.selectText.1.1=최적화 레벨 6에서 9에서는 일반적인 PDF 압축 외에도 이미지 해상도가 낮아져 파일 크기가 더욱 감소합니다. 높은 레벨은 더 강력한 이미지 압축(원본 크기의 최대 50%)을 초래하여 더 큰 크기 감소를 달성하지만 이미지 품질이 저하될 수 있습니다.
|
||||
compress.selectText.2=최적화 레벨:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=버전
|
||||
validateSignature.cert.keyUsage=키 용도
|
||||
validateSignature.cert.selfSigned=자체 서명
|
||||
validateSignature.cert.bits=비트
|
||||
compress.grayscale.label=압축을 위해 그레이스케일 적용
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Ongeldige gebruikersnaam of wachtwoord.
|
||||
login.locked=Je account is geblokkeerd.
|
||||
login.signinTitle=Gelieve in te loggen
|
||||
login.ssoSignIn=Inloggen via Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
|
||||
login.oauth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Automatisch aanmaken gebruiker uitgeschakeld
|
||||
login.oAuth2AdminBlockedUser=Registratie of inloggen van niet-registreerde gebruikers is helaas momenteel geblokkeerd. Neem contact op met de beheerder.
|
||||
login.oauth2RequestNotFound=Autorisatieverzoek niet gevonden
|
||||
login.oauth2InvalidUserInfoResponse=Ongeldige reactie op gebruikersinfo
|
||||
login.oauth2invalidRequest=Ongeldig verzoek
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Omzetten naar PDF
|
||||
compress.title=Comprimeren
|
||||
compress.header=PDF comprimeren
|
||||
compress.credit=Deze functie gebruikt qpdf voor PDF Compressie/Optimalisatie.
|
||||
compress.grayscale.label=Grijsschaal toepassen voor compressie
|
||||
compress.selectText.1=Handmatige modus - Van 1 tot 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimalisatieniveau:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Grijsschaal toepassen voor compressie
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Ugyldig brukernavn eller passord.
|
||||
login.locked=Kontoen din har blitt låst.
|
||||
login.signinTitle=Vennligst logg inn
|
||||
login.ssoSignIn=Logg inn via Enkel Pålogging
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-Opretting av bruker deaktivert
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Autentiseringsforespørsel ikke funnet
|
||||
login.oauth2InvalidUserInfoResponse=Ugyldig brukerinforespons
|
||||
login.oauth2invalidRequest=Ugyldig forespørsel
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konverter til PDF
|
||||
compress.title=Komprimer
|
||||
compress.header=Komprimer PDF
|
||||
compress.credit=Denne tjenesten bruker qpdf for PDF-komprimering/optimisering.
|
||||
compress.grayscale.label=Bruk gråskala for komprimering
|
||||
compress.selectText.1=Manuell modus - Fra 1 til 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimeringsnivå:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Bruk gråskala for komprimering
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nieprawidłowe dane logowania
|
||||
login.locked=Konto jest zablokowane
|
||||
login.signinTitle=Zaloguj się
|
||||
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
|
||||
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
|
||||
login.oauth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
|
||||
login.oAuth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
|
||||
login.oAuth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
|
||||
login.oauth2RequestNotFound=Błąd logowania OAuth2
|
||||
login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania
|
||||
login.oauth2invalidRequest=Nieprawidłowe żądanie
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konwertuj na PDF
|
||||
compress.title=Kompresuj
|
||||
compress.header=Kompresuj PDF
|
||||
compress.credit=Ta usługa używa qpdf do kompresji/optymalizacji PDF.
|
||||
compress.grayscale.label=Zastosuj skalę szarości do kompresji
|
||||
compress.selectText.1=Tryb ręczny - Od 1 do 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Poziom optymalizacji:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Zastosuj skalę szarości do kompresji
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Usuário ou senha inválidos.
|
||||
login.locked=Sua conta foi bloqueada.
|
||||
login.signinTitle=Por favor, inicie a sessão
|
||||
login.ssoSignIn=Iniciar sessão através de login único (SSO)
|
||||
login.oauth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
|
||||
login.oauth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
|
||||
login.oAuth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
|
||||
login.oAuth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
|
||||
login.oauth2RequestNotFound=Solicitação de autorização não encontrada
|
||||
login.oauth2InvalidUserInfoResponse=Resposta de informação de usuário inválida
|
||||
login.oauth2invalidRequest=Requisição Inválida
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Converter para PDF
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir
|
||||
compress.credit=Este serviço usa o Qpdf para compressão/otimização de PDF.
|
||||
compress.grayscale.label=Aplicar escala de cinza para compressão
|
||||
compress.selectText.1=Modo Manual - De 1 a 9
|
||||
compress.selectText.1.1=Nos níveis de otimização 6-9, além da compressão normal do PDF, a resolução das imagens são reduzidas, para diminuir ainda mais o tamanho do arquivo. Quanto maior o nível, maior a compressão da imagem (até 50% do tamanho original), resultando em tamanho menor do arquivo, porém com menor qualidade nas imagens.
|
||||
compress.selectText.2=Nível de Otimização:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versão
|
||||
validateSignature.cert.keyUsage=Uso da chave
|
||||
validateSignature.cert.selfSigned=Autoassinados
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplicar escala de cinza para compressão
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nome de utilizador ou palavra-passe inválidos.
|
||||
login.locked=A sua conta foi bloqueada.
|
||||
login.signinTitle=Por favor inicie sessão
|
||||
login.ssoSignIn=Login via Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada
|
||||
login.oauth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador.
|
||||
login.oAuth2AutoCreateDisabled=Criação Automática de Utilizador OAUTH2 Desativada
|
||||
login.oAuth2AdminBlockedUser=O registo ou login de utilizadores não registados está atualmente bloqueado. Por favor contacte o administrador.
|
||||
login.oauth2RequestNotFound=Pedido de autorização não encontrado
|
||||
login.oauth2InvalidUserInfoResponse=Resposta de Informação de Utilizador Inválida
|
||||
login.oauth2invalidRequest=Pedido Inválido
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Converter para PDF
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Este serviço usa qpdf para Compressão/Otimização de PDF.
|
||||
compress.grayscale.label=Aplicar escala de cinzentos para compressão
|
||||
compress.selectText.1=Modo Manual - De 1 a 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Nível de otimização:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Versão
|
||||
validateSignature.cert.keyUsage=Utilização da Chave
|
||||
validateSignature.cert.selfSigned=Auto-Assinado
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplicar escala de cinzentos para compressão
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Nume de utilizator sau parolă invalidă.
|
||||
login.locked=Contul tău a fost blocat.
|
||||
login.signinTitle=Te rugăm să te autentifici
|
||||
login.ssoSignIn=Conectare prin conectare unică
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
|
||||
login.oauth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Creare automată utilizator dezactivată
|
||||
login.oAuth2AdminBlockedUser=Înregistrarea sau conectarea utilizatorilor neînregistrați este în prezent blocată. Te rugăm să contactezi administratorul.
|
||||
login.oauth2RequestNotFound=Cererea de autorizare nu a fost găsită
|
||||
login.oauth2InvalidUserInfoResponse=Răspuns Invalid la Informațiile Utilizatorului
|
||||
login.oauth2invalidRequest=Cerere Invalidă
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Convertiți în PDF
|
||||
compress.title=Comprimare
|
||||
compress.header=Comprimare PDF
|
||||
compress.credit=Acest serviciu utilizează qpdf pentru comprimarea/optimizarea PDF-urilor.
|
||||
compress.grayscale.label=Aplicare scală de gri pentru compresie
|
||||
compress.selectText.1=Modul manual - de la 1 la 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Nivel de optimizare:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Aplicare scală de gri pentru compresie
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Неверное имя пользователя или парол
|
||||
login.locked=Ваша учетная запись заблокирована.
|
||||
login.signinTitle=Пожалуйста, войдите
|
||||
login.ssoSignIn=Вход через единый вход
|
||||
login.oauth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено
|
||||
login.oauth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору.
|
||||
login.oAuth2AutoCreateDisabled=Автоматическое создание пользователей OAuth2 отключено
|
||||
login.oAuth2AdminBlockedUser=Регистрация или вход незарегистрированных пользователей в настоящее время заблокированы. Обратитесь к администратору.
|
||||
login.oauth2RequestNotFound=Запрос авторизации не найден
|
||||
login.oauth2InvalidUserInfoResponse=Недействительный ответ с информацией о пользователе
|
||||
login.oauth2invalidRequest=Недействительный запрос
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Преобразовать в PDF
|
||||
compress.title=Сжать
|
||||
compress.header=Сжать PDF
|
||||
compress.credit=Этот сервис использует qpdf для сжатия/оптимизации PDF.
|
||||
compress.grayscale.label=Применить шкалу серого для сжатия
|
||||
compress.selectText.1=Ручной режим - от 1 до 5
|
||||
compress.selectText.1.1=На уровнях оптимизации от 6 до 9, помимо общего сжатия PDF, разрешение изображений уменьшается для дальнейшего сокращения размера файла. Более высокие уровни приводят к более сильному сжатию изображений (до 50% от исходного размера), обеспечивая большее уменьшение размера, но с возможной потерей качества изображений.
|
||||
compress.selectText.2=Уровень оптимизации:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Версия
|
||||
validateSignature.cert.keyUsage=Использование ключа
|
||||
validateSignature.cert.selfSigned=Самоподписанный
|
||||
validateSignature.cert.bits=бит
|
||||
compress.grayscale.label=Применить шкалу серого для сжатия
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Neplatné používateľské meno alebo heslo.
|
||||
login.locked=Váš účet bol uzamknutý.
|
||||
login.signinTitle=Prosím, prihláste sa
|
||||
login.ssoSignIn=Prihlásiť sa cez Single Sign-on
|
||||
login.oauth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=Vytváranie používateľa cez OAUTH2 je zakázané
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertovať do PDF
|
||||
compress.title=Komprimovať
|
||||
compress.header=Komprimovať PDF
|
||||
compress.credit=Táto služba používa qpdf pre kompresiu/optimalizáciu PDF.
|
||||
compress.grayscale.label=Použiť odtiene šedej na kompresiu
|
||||
compress.selectText.1=Manuálny režim - Od 1 do 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Úroveň optimalizácie:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Použiť odtiene šedej na kompresiu
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Neveljavno uporabniško ime ali geslo.
|
||||
login.locked=Vaš račun je bil zaklenjen.
|
||||
login.signinTitle=Prosim prijavite se
|
||||
login.ssoSignIn=Prijava prek enotne prijave
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno
|
||||
login.oauth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Samodejno ustvarjanje uporabnika onemogočeno
|
||||
login.oAuth2AdminBlockedUser=Registracija ali prijava neregistriranih uporabnikov je trenutno blokirana. Prosimo kontaktirajte skrbnika.
|
||||
login.oauth2RequestNotFound=Zahteva za avtorizacijo ni bila najdena
|
||||
login.oauth2InvalidUserInfoResponse=Neveljaven odgovor z informacijami o uporabniku
|
||||
login.oauth2invalidRequest=Neveljavna zahteva
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Pretvori v PDF
|
||||
compress.title=Stisnite
|
||||
compress.header=Stisnite PDF
|
||||
compress.credit=Ta storitev uporablja qpdf za stiskanje/optimizacijo PDF.
|
||||
compress.grayscale.label=Uporabi sivinsko lestvico za stiskanje
|
||||
compress.selectText.1=Ročni način - Od 1 do 5
|
||||
compress.selectText.1.1=Na stopnjah optimizacije od 6 do 9 je poleg splošnega stiskanja PDF ločljivost slike zmanjšana, da se dodatno zmanjša velikost datoteke. Višje ravni povzročijo močnejše stiskanje slike (do 50 % prvotne velikosti), s čimer se doseže večje zmanjšanje velikosti, vendar s potencialno izgubo kakovosti slik.
|
||||
compress.selectText.2=Raven optimizacije:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Različica
|
||||
validateSignature.cert.keyUsage=Uporaba ključa
|
||||
validateSignature.cert.selfSigned=Samopodpisano
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Uporabi sivinsko lestvico za stiskanje
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Neispravno korisničko ime ili lozinka.
|
||||
login.locked=Vaš nalog je zaključan.
|
||||
login.signinTitle=Molimo vas da se prijavite
|
||||
login.ssoSignIn=Prijavite se putem jedinstvene prijave
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
|
||||
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 automatsko kreiranje korisnika je onemogućeno
|
||||
login.oAuth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
|
||||
login.oauth2RequestNotFound=Authorization request not found
|
||||
login.oauth2InvalidUserInfoResponse=Invalid User Info Response
|
||||
login.oauth2invalidRequest=Invalid Request
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertuj u PDF
|
||||
compress.title=Kompresija
|
||||
compress.header=Kompresuj PDF
|
||||
compress.credit=Ova usluga koristi qpdf za kompresiju / optimizaciju PDF-a.
|
||||
compress.grayscale.label=Primeni sivinu za kompresiju
|
||||
compress.selectText.1=Ručni režim - Od 1 do 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Nivo optimizacije:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Primeni sivinu za kompresiju
|
||||
|
||||
@@ -572,8 +572,8 @@ login.invalid=Ogiltigt användarnamn eller lösenord.
|
||||
login.locked=Ditt konto har låsts.
|
||||
login.signinTitle=Vänligen logga in
|
||||
login.ssoSignIn=Logga in via enkel inloggning
|
||||
login.oauth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad
|
||||
login.oauth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören.
|
||||
login.oAuth2AutoCreateDisabled=OAUTH2 Auto-skapa användare inaktiverad
|
||||
login.oAuth2AdminBlockedUser=Registrering eller inloggning av icke-registrerade användare är för närvarande blockerad. Kontakta administratören.
|
||||
login.oauth2RequestNotFound=Auktoriseringsbegäran hittades inte
|
||||
login.oauth2InvalidUserInfoResponse=Ogiltigt svar på användarinformation
|
||||
login.oauth2invalidRequest=Ogiltig begäran
|
||||
@@ -951,6 +951,7 @@ fileToPDF.submit=Konvertera till PDF
|
||||
compress.title=Komprimera
|
||||
compress.header=Komprimera PDF
|
||||
compress.credit=Denna tjänst använder qpdf för PDF-komprimering/optimering.
|
||||
compress.grayscale.label=Tillämpa gråskala för komprimering
|
||||
compress.selectText.1=Manuellt läge - Från 1 till 5
|
||||
compress.selectText.1.1=In optimization levels 6 to 9, in addition to general PDF compression, image resolution is scaled down to further reduce file size. Higher levels result in stronger image compression (up to 50% of the original size), achieving greater size reduction but with potential quality loss in images.
|
||||
compress.selectText.2=Optimeringsnivå:
|
||||
@@ -1384,4 +1385,3 @@ validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
compress.grayscale.label=Tillämpa gråskala för komprimering
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user