Compare commits
22 Commits
main
...
saml2-sso-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92c0ddf63b | ||
|
|
7297e9e62d | ||
|
|
f8944fd2a9 | ||
|
|
3fd44fe7af | ||
|
|
4c9c9b5cbe | ||
|
|
e660237e28 | ||
|
|
83e93688ee | ||
|
|
dedfabd630 | ||
|
|
3b5b7772a9 | ||
|
|
c59d3ff3e0 | ||
|
|
5832147b30 | ||
|
|
20dc2f60cd | ||
|
|
b46ccdde44 | ||
|
|
4fa1b4adb0 | ||
|
|
03158b05e4 | ||
|
|
28c55ca80c | ||
|
|
6ca14edaf1 | ||
|
|
f9677b1fe8 | ||
|
|
04a6ebf515 | ||
|
|
262e2ed47a | ||
|
|
4436759e12 | ||
|
|
87925ac618 |
@@ -32,6 +32,12 @@ java {
|
|||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
|
maven {
|
||||||
|
url "https://build.shibboleth.net/nexus/content/repositories/releases/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url "https://build.shibboleth.net/maven/releases/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
licenseReport {
|
licenseReport {
|
||||||
@@ -127,6 +133,7 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-jetty:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
|
||||||
|
implementation 'com.posthog.java:posthog:1.1.1'
|
||||||
|
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||||
@@ -134,6 +141,8 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
|
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.3'
|
||||||
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
//2.2.x requires rebuild of DB file.. need migration path
|
||||||
runtimeOnly "com.h2database:h2:2.1.214"
|
runtimeOnly "com.h2database:h2:2.1.214"
|
||||||
// implementation "com.h2database:h2:2.2.224"
|
// implementation "com.h2database:h2:2.2.224"
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ Feature: API Validation
|
|||||||
| odt | .odt |
|
| odt | .odt |
|
||||||
| doc | .doc |
|
| doc | .doc |
|
||||||
|
|
||||||
@ocr
|
@ocr @pdfa1
|
||||||
Scenario: PDFA
|
Scenario: PDFA
|
||||||
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/pdfa2.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
@@ -134,7 +134,7 @@ Feature: API Validation
|
|||||||
And the response file should have extension ".pdf"
|
And the response file should have extension ".pdf"
|
||||||
And the response file should have size greater than 100
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
@ocr
|
@ocr @pdfa2
|
||||||
Scenario: PDFA1
|
Scenario: PDFA1
|
||||||
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/pdfa1.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ public class EEAppConfig {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Bean(name = "RunningEE")
|
@Autowired private LicenseKeyChecker licenseKeyChecker;
|
||||||
|
|
||||||
|
@Bean(name = "runningEE")
|
||||||
public boolean runningEnterpriseEdition() {
|
public boolean runningEnterpriseEdition() {
|
||||||
// TODO: Implement EE detection
|
return licenseKeyChecker.getEnterpriseEnabledResult();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
package stirling.software.SPDF.EE;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.posthog.java.shaded.org.json.JSONObject;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class KeygenLicenseVerifier {
|
||||||
|
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
|
||||||
|
private static final String PRODUCT_ID = "f9bb2423-62c9-4d39-8def-4fdc5aca751e";
|
||||||
|
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
|
||||||
|
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 23:26:20.344 [scheduling-1] INFO s.s.SPDF.EE.KeygenLicenseVerifier -
|
||||||
|
// validateLicenseResponse body:
|
||||||
|
// {"data":{"id":"808ed3c9-584b-46dd-8a80-c9217ef70915","type":"licenses","attributes":{"name":"userCounTest","key":"A7EW-KUPF-PRML-RRVL-HLMP-7THR-F7KE-XF4C","expiry":"2024-10-31T21:39:49.271Z","status":"ACTIVE","uses":0,"suspended":false,"scheme":null,"encrypted":false,"strict":true,"floating":true,"protected":true,"version":null,"maxMachines":1,"maxProcesses":null,"maxUsers":null,"maxCores":null,"maxUses":null,"requireHeartbeat":false,"requireCheckIn":false,"lastValidated":"2024-10-01T22:26:18.121Z","lastCheckIn":null,"nextCheckIn":null,"lastCheckOut":null,"metadata":{"users":10},"created":"2024-10-01T21:39:49.268Z","updated":"2024-10-01T21:39:49.268Z"},"relationships":{"account":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372"},"data":{"type":"accounts","id":"e5430f69-e834-4ae4-befd-b602aae5f372"}},"environment":{"links":{"related":null},"data":null},"product":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/product"},"data":{"type":"products","id":"f9bb2423-62c9-4d39-8def-4fdc5aca751e"}},"policy":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/policy"},"data":{"type":"policies","id":"04caef06-9ac2-4084-bf3c-bca4a0d29143"}},"group":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/group"},"data":null},"owner":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/owner"},"data":null},"users":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/users"},"meta":{"count":0}},"machines":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/machines"},"meta":{"cores":0,"count":0}},"tokens":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/tokens"}},"entitlements":{"links":{"related":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915/entitlements"}}},"links":{"self":"/v1/accounts/e5430f69-e834-4ae4-befd-b602aae5f372/licenses/808ed3c9-584b-46dd-8a80-c9217ef70915"}},"meta":{"ts":"2024-10-01T22:26:18.124Z","valid":false,"detail":"fingerprint is not activated (has no associated machines)","code":"NO_MACHINES","scope":{"fingerprint":"example-fingerprint"}}}
|
||||||
|
|
||||||
|
public boolean verifyLicense(String licenseKey) {
|
||||||
|
try {
|
||||||
|
log.info("Checking license key");
|
||||||
|
String machineFingerprint = generateMachineFingerprint();
|
||||||
|
|
||||||
|
// First, try to validate the license
|
||||||
|
JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint);
|
||||||
|
if (validationResponse != null) {
|
||||||
|
boolean isValid = validationResponse.path("meta").path("valid").asBoolean();
|
||||||
|
String licenseId = validationResponse.path("data").path("id").asText();
|
||||||
|
if (!isValid) {
|
||||||
|
String code = validationResponse.path("meta").path("code").asText();
|
||||||
|
log.debug(code);
|
||||||
|
if ("NO_MACHINE".equals(code)
|
||||||
|
|| "NO_MACHINES".equals(code)
|
||||||
|
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
|
||||||
|
log.info(
|
||||||
|
"License not activated for this machine. Attempting to activate...");
|
||||||
|
boolean activated =
|
||||||
|
activateMachine(licenseKey, licenseId, machineFingerprint);
|
||||||
|
if (activated) {
|
||||||
|
// Revalidate after activation
|
||||||
|
validationResponse = validateLicense(licenseKey, machineFingerprint);
|
||||||
|
isValid =
|
||||||
|
validationResponse != null
|
||||||
|
&& validationResponse
|
||||||
|
.path("meta")
|
||||||
|
.path("valid")
|
||||||
|
.asBoolean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error verifying license: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonNode validateLicense(String licenseKey, String machineFingerprint)
|
||||||
|
throws Exception {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
String requestBody =
|
||||||
|
String.format(
|
||||||
|
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
||||||
|
licenseKey, machineFingerprint);
|
||||||
|
HttpRequest request =
|
||||||
|
HttpRequest.newBuilder()
|
||||||
|
.uri(
|
||||||
|
URI.create(
|
||||||
|
BASE_URL
|
||||||
|
+ "/"
|
||||||
|
+ ACCOUNT_ID
|
||||||
|
+ "/licenses/actions/validate-key"))
|
||||||
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
.header("Accept", "application/vnd.api+json")
|
||||||
|
// .header("Authorization", "License " + licenseKey)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
log.info(" 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();
|
||||||
|
|
||||||
|
String detail = metaNode.path("detail").asText();
|
||||||
|
String code = metaNode.path("code").asText();
|
||||||
|
|
||||||
|
log.debug("License validity: " + isValid);
|
||||||
|
log.debug("Validation detail: " + detail);
|
||||||
|
log.debug("Validation code: " + code);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
log.error("Error validating license. Status code: " + response.statusCode());
|
||||||
|
}
|
||||||
|
return jsonResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean activateMachine(
|
||||||
|
String licenseKey, String licenseId, String machineFingerprint) throws Exception {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
|
||||||
|
String hostname;
|
||||||
|
try {
|
||||||
|
hostname = java.net.InetAddress.getLocalHost().getHostName();
|
||||||
|
} catch (Exception e) {
|
||||||
|
hostname = "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject body =
|
||||||
|
new JSONObject()
|
||||||
|
.put(
|
||||||
|
"data",
|
||||||
|
new JSONObject()
|
||||||
|
.put("type", "machines")
|
||||||
|
.put(
|
||||||
|
"attributes",
|
||||||
|
new JSONObject()
|
||||||
|
.put("fingerprint", machineFingerprint)
|
||||||
|
.put(
|
||||||
|
"platform",
|
||||||
|
System.getProperty(
|
||||||
|
"os.name")) // Added
|
||||||
|
// platform
|
||||||
|
// parameter
|
||||||
|
.put(
|
||||||
|
"name",
|
||||||
|
hostname)) // Added name parameter
|
||||||
|
.put(
|
||||||
|
"relationships",
|
||||||
|
new JSONObject()
|
||||||
|
.put(
|
||||||
|
"license",
|
||||||
|
new JSONObject()
|
||||||
|
.put(
|
||||||
|
"data",
|
||||||
|
new JSONObject()
|
||||||
|
.put(
|
||||||
|
"type",
|
||||||
|
"licenses")
|
||||||
|
.put(
|
||||||
|
"id",
|
||||||
|
licenseId)))));
|
||||||
|
|
||||||
|
HttpRequest request =
|
||||||
|
HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
|
||||||
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
|
.header("Accept", "application/vnd.api+json")
|
||||||
|
.header(
|
||||||
|
"Authorization",
|
||||||
|
"License " + licenseKey) // Keep the license key authentication
|
||||||
|
.POST(
|
||||||
|
HttpRequest.BodyPublishers.ofString(
|
||||||
|
body.toString())) // Send the JSON body
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
log.debug("activateMachine Response body: " + response.body());
|
||||||
|
if (response.statusCode() == 201) {
|
||||||
|
log.info("Machine activated successfully");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
"Error activating machine. Status code: {}, error: {}",
|
||||||
|
response.statusCode(),
|
||||||
|
response.body());
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String generateMachineFingerprint() {
|
||||||
|
return GeneralUtils.generateMachineFingerprint();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package stirling.software.SPDF.EE;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class LicenseKeyChecker {
|
||||||
|
|
||||||
|
private final KeygenLicenseVerifier licenseService;
|
||||||
|
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
private boolean enterpriseEnbaledResult = false;
|
||||||
|
|
||||||
|
// Inject your license service or configuration
|
||||||
|
@Autowired
|
||||||
|
public LicenseKeyChecker(
|
||||||
|
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
||||||
|
this.licenseService = licenseService;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
|
||||||
|
public void checkLicensePeriodically() {
|
||||||
|
checkLicense();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkLicense() {
|
||||||
|
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||||
|
enterpriseEnbaledResult = false;
|
||||||
|
} else {
|
||||||
|
enterpriseEnbaledResult =
|
||||||
|
licenseService.verifyLicense(
|
||||||
|
applicationProperties.getEnterpriseEdition().getKey());
|
||||||
|
if (enterpriseEnbaledResult) {
|
||||||
|
log.info("License key is valid.");
|
||||||
|
} else {
|
||||||
|
log.info("License key is invalid.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateLicenseKey(String newKey) throws IOException {
|
||||||
|
applicationProperties.getEnterpriseEdition().setKey(newKey);
|
||||||
|
GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false);
|
||||||
|
checkLicense();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getEnterpriseEnabledResult() {
|
||||||
|
return enterpriseEnbaledResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class LibreOfficeListener {
|
public class LibreOfficeListener {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
|
||||||
@@ -31,7 +34,7 @@ public class LibreOfficeListener {
|
|||||||
private LibreOfficeListener() {}
|
private LibreOfficeListener() {}
|
||||||
|
|
||||||
private boolean isListenerRunning() {
|
private boolean isListenerRunning() {
|
||||||
System.out.println("waiting for listener to start");
|
log.info("waiting for listener to start");
|
||||||
try (Socket socket = new Socket()) {
|
try (Socket socket = new Socket()) {
|
||||||
socket.connect(
|
socket.connect(
|
||||||
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||||
|
|||||||
@@ -160,4 +160,27 @@ public class AppConfig {
|
|||||||
public String accessibilityStatement() {
|
public String accessibilityStatement() {
|
||||||
return applicationProperties.getLegal().getAccessibilityStatement();
|
return applicationProperties.getLegal().getAccessibilityStatement();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(name = "analyticsPrompt")
|
||||||
|
public boolean analyticsPrompt() {
|
||||||
|
return applicationProperties.getSystem().getEnableAnalytics() == null
|
||||||
|
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "analyticsEnabled")
|
||||||
|
public boolean analyticsEnabled() {
|
||||||
|
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
|
||||||
|
return applicationProperties.getSystem().getEnableAnalytics() != null
|
||||||
|
&& Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "StirlingPDFLabel")
|
||||||
|
public String stirlingPDFLabel() {
|
||||||
|
return "Stirling-PDF" + " v" + appVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean(name = "UUID")
|
||||||
|
public String uuid() {
|
||||||
|
return applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean;
|
|||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||||
|
public class InitialSetup {
|
||||||
|
|
||||||
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initUUIDKey() throws IOException {
|
||||||
|
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||||
|
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||||
|
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
|
||||||
|
applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initSecretKey() throws IOException {
|
||||||
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
|
if (!GeneralUtils.isValidUUID(secretKey)) {
|
||||||
|
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
||||||
|
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
|
||||||
|
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
|||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class Beans implements WebMvcConfigurer {
|
public class LocaleConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -32,10 +33,11 @@ public class MetricsFilter extends OncePerRequestFilter {
|
|||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
|
|
||||||
if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) {
|
if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
String sessionId = (session != null) ? session.getId() : "no-session";
|
||||||
Counter counter =
|
Counter counter =
|
||||||
Counter.builder("http.requests")
|
Counter.builder("http.requests")
|
||||||
.tag("session", request.getSession().getId())
|
.tag("session", sessionId)
|
||||||
.tag("method", request.getMethod())
|
.tag("method", request.getMethod())
|
||||||
.tag("uri", uri)
|
.tag("uri", uri)
|
||||||
.register(meterRegistry);
|
.register(meterRegistry);
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
|
import jakarta.annotation.PreDestroy;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class PostHogConfig {
|
||||||
|
|
||||||
|
@Value("${posthog.api.key}")
|
||||||
|
private String posthogApiKey;
|
||||||
|
|
||||||
|
@Value("${posthog.host}")
|
||||||
|
private String posthogHost;
|
||||||
|
|
||||||
|
private PostHog postHogClient;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PostHog postHogClient() {
|
||||||
|
postHogClient = new PostHog.Builder(posthogApiKey).host(posthogHost).build();
|
||||||
|
return postHogClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdownPostHog() {
|
||||||
|
if (postHogClient != null) {
|
||||||
|
postHogClient.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// package stirling.software.SPDF.config.fingerprint;
|
||||||
|
//
|
||||||
|
// import java.io.IOException;
|
||||||
|
//
|
||||||
|
// import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
// import org.springframework.stereotype.Component;
|
||||||
|
// import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
//
|
||||||
|
// import jakarta.servlet.FilterChain;
|
||||||
|
// import jakarta.servlet.ServletException;
|
||||||
|
// import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
// import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
// import jakarta.servlet.http.HttpSession;
|
||||||
|
// import lombok.extern.slf4j.Slf4j;
|
||||||
|
// import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
//
|
||||||
|
//// @Component
|
||||||
|
// @Slf4j
|
||||||
|
// public class FingerprintBasedSessionFilter extends OncePerRequestFilter {
|
||||||
|
// private final FingerprintGenerator fingerprintGenerator;
|
||||||
|
// private final FingerprintBasedSessionManager sessionManager;
|
||||||
|
//
|
||||||
|
// @Autowired
|
||||||
|
// public FingerprintBasedSessionFilter(
|
||||||
|
// FingerprintGenerator fingerprintGenerator,
|
||||||
|
// FingerprintBasedSessionManager sessionManager) {
|
||||||
|
// this.fingerprintGenerator = fingerprintGenerator;
|
||||||
|
// this.sessionManager = sessionManager;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// protected void doFilterInternal(
|
||||||
|
// HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
// throws ServletException, IOException {
|
||||||
|
//
|
||||||
|
// if (RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
||||||
|
// filterChain.doFilter(request, response);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// String fingerprint = fingerprintGenerator.generateFingerprint(request);
|
||||||
|
// log.debug("Generated fingerprint for request: {}", fingerprint);
|
||||||
|
//
|
||||||
|
// HttpSession session = request.getSession();
|
||||||
|
// boolean isNewSession = session.isNew();
|
||||||
|
// String sessionId = session.getId();
|
||||||
|
//
|
||||||
|
// if (isNewSession) {
|
||||||
|
// log.info("New session created: {}", sessionId);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (!sessionManager.isFingerPrintAllowed(fingerprint)) {
|
||||||
|
// log.info("Blocked fingerprint detected, redirecting: {}", fingerprint);
|
||||||
|
// response.sendRedirect(request.getContextPath() + "/too-many-requests");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// session.setAttribute("userFingerprint", fingerprint);
|
||||||
|
// session.setAttribute(
|
||||||
|
// FingerprintBasedSessionManager.STARTUP_TIMESTAMP,
|
||||||
|
// FingerprintBasedSessionManager.APP_STARTUP_TIME);
|
||||||
|
//
|
||||||
|
// sessionManager.registerFingerprint(fingerprint, sessionId);
|
||||||
|
//
|
||||||
|
// log.debug("Proceeding with request: {}", request.getRequestURI());
|
||||||
|
// filterChain.doFilter(request, response);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// package stirling.software.SPDF.config.fingerprint;
|
||||||
|
//
|
||||||
|
// import java.util.Iterator;
|
||||||
|
// import java.util.Map;
|
||||||
|
// import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
// import java.util.concurrent.TimeUnit;
|
||||||
|
//
|
||||||
|
// import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
// import org.springframework.stereotype.Component;
|
||||||
|
//
|
||||||
|
// import jakarta.servlet.http.HttpSession;
|
||||||
|
// import jakarta.servlet.http.HttpSessionAttributeListener;
|
||||||
|
// import jakarta.servlet.http.HttpSessionEvent;
|
||||||
|
// import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
// import lombok.AllArgsConstructor;
|
||||||
|
// import lombok.Data;
|
||||||
|
// import lombok.extern.slf4j.Slf4j;
|
||||||
|
//
|
||||||
|
// @Slf4j
|
||||||
|
// @Component
|
||||||
|
// public class FingerprintBasedSessionManager
|
||||||
|
// implements HttpSessionListener, HttpSessionAttributeListener {
|
||||||
|
// private static final ConcurrentHashMap<String, FingerprintInfo> activeFingerprints =
|
||||||
|
// new ConcurrentHashMap<>();
|
||||||
|
//
|
||||||
|
// // To be reduced in later version to 8~
|
||||||
|
// private static final int MAX_ACTIVE_FINGERPRINTS = 30;
|
||||||
|
//
|
||||||
|
// static final String STARTUP_TIMESTAMP = "appStartupTimestamp";
|
||||||
|
// static final long APP_STARTUP_TIME = System.currentTimeMillis();
|
||||||
|
// private static final long FINGERPRINT_EXPIRATION = TimeUnit.MINUTES.toMillis(30);
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void sessionCreated(HttpSessionEvent se) {
|
||||||
|
// HttpSession session = se.getSession();
|
||||||
|
// String sessionId = session.getId();
|
||||||
|
// String fingerprint = (String) session.getAttribute("userFingerprint");
|
||||||
|
//
|
||||||
|
// if (fingerprint == null) {
|
||||||
|
// log.warn("Session created without fingerprint: {}", sessionId);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// if (activeFingerprints.size() >= MAX_ACTIVE_FINGERPRINTS
|
||||||
|
// && !activeFingerprints.containsKey(fingerprint)) {
|
||||||
|
// log.info("Max fingerprints reached. Marking session as blocked: {}", sessionId);
|
||||||
|
// session.setAttribute("blocked", true);
|
||||||
|
// } else {
|
||||||
|
// activeFingerprints.put(
|
||||||
|
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
|
||||||
|
// log.info(
|
||||||
|
// "New fingerprint registered: {}. Total active fingerprints: {}",
|
||||||
|
// fingerprint,
|
||||||
|
// activeFingerprints.size());
|
||||||
|
// }
|
||||||
|
// session.setAttribute(STARTUP_TIMESTAMP, APP_STARTUP_TIME);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
|
// HttpSession session = se.getSession();
|
||||||
|
// String fingerprint = (String) session.getAttribute("userFingerprint");
|
||||||
|
//
|
||||||
|
// if (fingerprint != null) {
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// activeFingerprints.remove(fingerprint);
|
||||||
|
// log.info(
|
||||||
|
// "Fingerprint removed: {}. Total active fingerprints: {}",
|
||||||
|
// fingerprint,
|
||||||
|
// activeFingerprints.size());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public boolean isFingerPrintAllowed(String fingerprint) {
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// return activeFingerprints.size() < MAX_ACTIVE_FINGERPRINTS
|
||||||
|
// || activeFingerprints.containsKey(fingerprint);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void registerFingerprint(String fingerprint, String sessionId) {
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// activeFingerprints.put(
|
||||||
|
// fingerprint, new FingerprintInfo(sessionId, System.currentTimeMillis()));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void unregisterFingerprint(String fingerprint) {
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// activeFingerprints.remove(fingerprint);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Scheduled(fixedRate = 1800000) // Run every 30 mins
|
||||||
|
// public void cleanupStaleFingerprints() {
|
||||||
|
// log.info("Starting cleanup of stale fingerprints");
|
||||||
|
// long now = System.currentTimeMillis();
|
||||||
|
// int removedCount = 0;
|
||||||
|
//
|
||||||
|
// synchronized (activeFingerprints) {
|
||||||
|
// Iterator<Map.Entry<String, FingerprintInfo>> iterator =
|
||||||
|
// activeFingerprints.entrySet().iterator();
|
||||||
|
// while (iterator.hasNext()) {
|
||||||
|
// Map.Entry<String, FingerprintInfo> entry = iterator.next();
|
||||||
|
// FingerprintInfo info = entry.getValue();
|
||||||
|
//
|
||||||
|
// if (now - info.getLastAccessTime() > FINGERPRINT_EXPIRATION) {
|
||||||
|
// iterator.remove();
|
||||||
|
// removedCount++;
|
||||||
|
// log.info("Removed stale fingerprint: {}", entry.getKey());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// log.info("Cleanup complete. Removed {} stale fingerprints", removedCount);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// public void updateLastAccessTime(String fingerprint) {
|
||||||
|
// FingerprintInfo info = activeFingerprints.get(fingerprint);
|
||||||
|
// if (info != null) {
|
||||||
|
// info.setLastAccessTime(System.currentTimeMillis());
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Data
|
||||||
|
// @AllArgsConstructor
|
||||||
|
// private static class FingerprintInfo {
|
||||||
|
// private String sessionId;
|
||||||
|
// private long lastAccessTime;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// package stirling.software.SPDF.config.fingerprint;
|
||||||
|
//
|
||||||
|
// import java.security.MessageDigest;
|
||||||
|
// import java.security.NoSuchAlgorithmException;
|
||||||
|
//
|
||||||
|
// import org.springframework.stereotype.Component;
|
||||||
|
//
|
||||||
|
// import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
//
|
||||||
|
// @Component
|
||||||
|
// public class FingerprintGenerator {
|
||||||
|
//
|
||||||
|
// public String generateFingerprint(HttpServletRequest request) {
|
||||||
|
// if (request == null) {
|
||||||
|
// return "";
|
||||||
|
// }
|
||||||
|
// StringBuilder fingerprintBuilder = new StringBuilder();
|
||||||
|
//
|
||||||
|
// // Add IP address
|
||||||
|
// fingerprintBuilder.append(request.getRemoteAddr());
|
||||||
|
//
|
||||||
|
// // Add X-Forwarded-For header if present (for clients behind proxies)
|
||||||
|
// String forwardedFor = request.getHeader("X-Forwarded-For");
|
||||||
|
// if (forwardedFor != null) {
|
||||||
|
// fingerprintBuilder.append(forwardedFor);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Add User-Agent
|
||||||
|
// String userAgent = request.getHeader("User-Agent");
|
||||||
|
// if (userAgent != null) {
|
||||||
|
// fingerprintBuilder.append(userAgent);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Add Accept-Language header
|
||||||
|
// String acceptLanguage = request.getHeader("Accept-Language");
|
||||||
|
// if (acceptLanguage != null) {
|
||||||
|
// fingerprintBuilder.append(acceptLanguage);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Add Accept header
|
||||||
|
// String accept = request.getHeader("Accept");
|
||||||
|
// if (accept != null) {
|
||||||
|
// fingerprintBuilder.append(accept);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Add Connection header
|
||||||
|
// String connection = request.getHeader("Connection");
|
||||||
|
// if (connection != null) {
|
||||||
|
// fingerprintBuilder.append(connection);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Add server port
|
||||||
|
// fingerprintBuilder.append(request.getServerPort());
|
||||||
|
//
|
||||||
|
// // Add secure flag
|
||||||
|
// fingerprintBuilder.append(request.isSecure());
|
||||||
|
//
|
||||||
|
// // Generate a hash of the fingerprint
|
||||||
|
// return generateHash(fingerprintBuilder.toString());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// private String generateHash(String input) {
|
||||||
|
// try {
|
||||||
|
// MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
// byte[] hash = digest.digest(input.getBytes());
|
||||||
|
// StringBuilder hexString = new StringBuilder();
|
||||||
|
// for (byte b : hash) {
|
||||||
|
// String hex = Integer.toHexString(0xff & b);
|
||||||
|
// if (hex.length() == 1) hexString.append('0');
|
||||||
|
// hexString.append(hex);
|
||||||
|
// }
|
||||||
|
// return hexString.toString();
|
||||||
|
// } catch (NoSuchAlgorithmException e) {
|
||||||
|
// throw new RuntimeException("Failed to generate fingerprint hash", e);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config.interfaces;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config.interfaces;
|
||||||
|
|
||||||
public interface ShowAdminInterface {
|
public interface ShowAdminInterface {
|
||||||
default boolean getShowUpdateOnlyAdmins() {
|
default boolean getShowUpdateOnlyAdmins() {
|
||||||
@@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.ShowAdminInterface;
|
import stirling.software.SPDF.config.interfaces.ShowAdminInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.repository.UserRepository;
|
import stirling.software.SPDF.repository.UserRepository;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -14,9 +16,12 @@ import jakarta.servlet.FilterChain;
|
|||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class FirstLoginFilter extends OncePerRequestFilter {
|
public class FirstLoginFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@@ -50,6 +55,22 @@ public class FirstLoginFilter extends OncePerRequestFilter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
HttpSession session = request.getSession(true);
|
||||||
|
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
|
||||||
|
String creationTime = timeFormat.format(new Date(session.getCreationTime()));
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
|
||||||
|
session.isNew(),
|
||||||
|
creationTime,
|
||||||
|
session.getId(),
|
||||||
|
request.getRemoteAddr(),
|
||||||
|
request.getHeader("User-Agent"),
|
||||||
|
request.getHeader("Referer"),
|
||||||
|
request.getRequestURL().toString());
|
||||||
|
}
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.simpleyaml.configuration.file.YamlFile;
|
|
||||||
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
|
||||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.Role;
|
import stirling.software.SPDF.model.Role;
|
||||||
|
|
||||||
@@ -39,15 +34,6 @@ public class InitialSecuritySetup {
|
|||||||
initializeInternalApiUser();
|
initializeInternalApiUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initSecretKey() throws IOException {
|
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
|
||||||
if (!isValidUUID(secretKey)) {
|
|
||||||
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
|
|
||||||
saveKeyToConfig(secretKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeAdminUser() throws IOException {
|
private void initializeAdminUser() throws IOException {
|
||||||
String initialUsername =
|
String initialUsername =
|
||||||
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
applicationProperties.getSecurity().getInitialLogin().getUsername();
|
||||||
@@ -89,33 +75,4 @@ public class InitialSecuritySetup {
|
|||||||
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveKeyToConfig(String key) throws IOException {
|
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
|
||||||
|
|
||||||
final YamlFile settingsYml = new YamlFile(path.toFile());
|
|
||||||
DumperOptions yamlOptionssettingsYml =
|
|
||||||
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
|
||||||
yamlOptionssettingsYml.setSplitLines(false);
|
|
||||||
|
|
||||||
settingsYml.loadWithComments();
|
|
||||||
|
|
||||||
settingsYml
|
|
||||||
.path("AutomaticallyGenerated.key")
|
|
||||||
.set(key)
|
|
||||||
.comment("# Automatically Generated Settings (Do Not Edit Directly)");
|
|
||||||
settingsYml.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidUUID(String uuid) {
|
|
||||||
if (uuid == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
UUID.fromString(uuid);
|
|
||||||
return true;
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,58 @@
|
|||||||
package stirling.software.SPDF.config.security;
|
package stirling.software.SPDF.config.security;
|
||||||
|
|
||||||
import java.util.*;
|
import static org.springframework.security.config.Customizer.withDefaults;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
||||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
|
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
|
||||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
|
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||||
|
import stirling.software.SPDF.config.security.saml.ConvertResponseToAuthentication;
|
||||||
|
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler;
|
||||||
|
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
|
|
||||||
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.JPATokenRepositoryImpl;
|
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
|
@Slf4j
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SecurityConfiguration.class);
|
@Autowired(required = false)
|
||||||
|
private GrantedAuthoritiesMapper userAuthoritiesMapper;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
@@ -73,12 +74,15 @@ public class SecurityConfiguration {
|
|||||||
@Autowired private FirstLoginFilter firstLoginFilter;
|
@Autowired private FirstLoginFilter firstLoginFilter;
|
||||||
@Autowired private SessionPersistentRegistry sessionRegistry;
|
@Autowired private SessionPersistentRegistry sessionRegistry;
|
||||||
|
|
||||||
|
@Autowired private ConvertResponseToAuthentication convertResponseToAuthentication;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
http.authenticationManager(authenticationManager(http));
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
|
http.addFilterBefore(
|
||||||
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
http.csrf(csrf -> csrf.disable());
|
http.csrf(csrf -> csrf.disable());
|
||||||
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
@@ -135,6 +139,7 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
return trimmedUri.startsWith("/login")
|
return trimmedUri.startsWith("/login")
|
||||||
|| trimmedUri.startsWith("/oauth")
|
|| trimmedUri.startsWith("/oauth")
|
||||||
|
|| trimmedUri.startsWith("/saml2")
|
||||||
|| trimmedUri.endsWith(".svg")
|
|| trimmedUri.endsWith(".svg")
|
||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/register")
|
"/register")
|
||||||
@@ -184,13 +189,37 @@ public class SecurityConfiguration {
|
|||||||
userService,
|
userService,
|
||||||
loginAttemptService))
|
loginAttemptService))
|
||||||
.userAuthoritiesMapper(
|
.userAuthoritiesMapper(
|
||||||
userAuthoritiesMapper())))
|
userAuthoritiesMapper)))
|
||||||
.logout(
|
.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutSuccessHandler(
|
logout.logoutSuccessHandler(
|
||||||
new CustomOAuth2LogoutSuccessHandler(
|
new CustomOAuth2LogoutSuccessHandler(
|
||||||
applicationProperties)));
|
applicationProperties)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle SAML
|
||||||
|
if (applicationProperties.getSecurity().getSaml() != null
|
||||||
|
&& applicationProperties.getSecurity().getSaml().getEnabled()
|
||||||
|
&& !applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getLoginMethod()
|
||||||
|
.equalsIgnoreCase("normal")) {
|
||||||
|
http.saml2Login(
|
||||||
|
saml2 -> {
|
||||||
|
saml2.relyingPartyRegistrationRepository(
|
||||||
|
relyingPartyRegistrationRepository)
|
||||||
|
.successHandler(
|
||||||
|
new CustomSAMLAuthenticationSuccessHandler(
|
||||||
|
loginAttemptService,
|
||||||
|
userService,
|
||||||
|
applicationProperties))
|
||||||
|
.failureHandler(
|
||||||
|
new CustomSAMLAuthenticationFailureHandler());
|
||||||
|
})
|
||||||
|
.saml2Logout(withDefaults())
|
||||||
|
.addFilterBefore(
|
||||||
|
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
http.csrf(csrf -> csrf.disable())
|
http.csrf(csrf -> csrf.disable())
|
||||||
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
@@ -199,176 +228,32 @@ public class SecurityConfiguration {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client Registration Repository for OAUTH2 OIDC Login
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(
|
public AuthenticationProvider samlAuthenticationProvider() {
|
||||||
value = "security.oauth2.enabled",
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
havingValue = "true",
|
new OpenSaml4AuthenticationProvider();
|
||||||
matchIfMissing = false)
|
authenticationProvider.setResponseAuthenticationConverter(convertResponseToAuthentication);
|
||||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
return authenticationProvider;
|
||||||
List<ClientRegistration> registrations = new ArrayList<>();
|
|
||||||
|
|
||||||
githubClientRegistration().ifPresent(registrations::add);
|
|
||||||
oidcClientRegistration().ifPresent(registrations::add);
|
|
||||||
googleClientRegistration().ifPresent(registrations::add);
|
|
||||||
keycloakClientRegistration().ifPresent(registrations::add);
|
|
||||||
|
|
||||||
if (registrations.isEmpty()) {
|
|
||||||
logger.error("At least one OAuth2 provider must be configured");
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
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()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
KeycloakProvider keycloak = client.getKeycloak();
|
|
||||||
|
|
||||||
return keycloak != null && keycloak.isSettingsValid()
|
|
||||||
? Optional.of(
|
|
||||||
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
|
|
||||||
.registrationId(keycloak.getName())
|
|
||||||
.clientId(keycloak.getClientId())
|
|
||||||
.clientSecret(keycloak.getClientSecret())
|
|
||||||
.scope(keycloak.getScopes())
|
|
||||||
.userNameAttributeName(keycloak.getUseAsUsername())
|
|
||||||
.clientName(keycloak.getClientName())
|
|
||||||
.build())
|
|
||||||
: Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<ClientRegistration> githubClientRegistration() {
|
|
||||||
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
|
|
||||||
if (oauth == null || !oauth.getEnabled()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
Client client = oauth.getClient();
|
|
||||||
if (client == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
GithubProvider github = client.getGithub();
|
|
||||||
return github != null && github.isSettingsValid()
|
|
||||||
? 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())
|
|
||||||
.clientName(github.getClientName())
|
|
||||||
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
|
|
||||||
.authorizationGrantType(
|
|
||||||
org.springframework.security.oauth2.core
|
|
||||||
.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()) {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
@Bean
|
||||||
@ConditionalOnProperty(
|
public AuthenticationProvider daoAuthenticationProvider() {
|
||||||
value = "security.oauth2.enabled",
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
havingValue = "true",
|
provider.setUserDetailsService(userDetailsService); // UserDetailsService
|
||||||
matchIfMissing = false)
|
provider.setPasswordEncoder(passwordEncoder()); // PasswordEncoder
|
||||||
GrantedAuthoritiesMapper userAuthoritiesMapper() {
|
return provider;
|
||||||
return (authorities) -> {
|
}
|
||||||
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
|
|
||||||
|
|
||||||
authorities.forEach(
|
@Bean
|
||||||
authority -> {
|
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
|
||||||
// Add existing OAUTH2 Authorities
|
AuthenticationManagerBuilder authenticationManagerBuilder =
|
||||||
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
|
http.getSharedObject(AuthenticationManagerBuilder.class);
|
||||||
|
|
||||||
// Add Authorities from database for existing user, if user is present.
|
authenticationManagerBuilder
|
||||||
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
|
.authenticationProvider(daoAuthenticationProvider()) // Benutzername/Passwort
|
||||||
String useAsUsername =
|
.authenticationProvider(samlAuthenticationProvider()); // SAML
|
||||||
applicationProperties
|
|
||||||
.getSecurity()
|
return authenticationManagerBuilder.build();
|
||||||
.getOauth2()
|
|
||||||
.getUseAsUsername();
|
|
||||||
Optional<User> userOpt =
|
|
||||||
userService.findByUsernameIgnoreCase(
|
|
||||||
(String) oauth2Auth.getAttributes().get(useAsUsername));
|
|
||||||
if (userOpt.isPresent()) {
|
|
||||||
User user = userOpt.get();
|
|
||||||
if (user != null) {
|
|
||||||
mappedAuthorities.add(
|
|
||||||
new SimpleGrantedAuthority(
|
|
||||||
userService.findRole(user).getAuthority()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return mappedAuthorities;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -386,4 +271,13 @@ public class SecurityConfiguration {
|
|||||||
public boolean activSecurity() {
|
public boolean activSecurity() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only Dev test
|
||||||
|
@Bean
|
||||||
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
|
return (web) ->
|
||||||
|
web.ignoring()
|
||||||
|
.requestMatchers(
|
||||||
|
"/css/**", "/images/**", "/js/**", "/**.svg", "/pdfjs-legacy/**");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import java.util.List;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -30,13 +29,18 @@ import stirling.software.SPDF.model.User;
|
|||||||
@Component
|
@Component
|
||||||
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
@Autowired @Lazy private UserService userService;
|
private final UserService userService;
|
||||||
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
public UserAuthenticationFilter(
|
||||||
|
@Lazy UserService userService,
|
||||||
@Autowired
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||||
public boolean loginEnabledValue;
|
this.userService = userService;
|
||||||
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
@@ -51,6 +55,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
// Check for session expiration (unsure if needed)
|
||||||
|
// if (authentication != null && authentication.isAuthenticated()) {
|
||||||
|
// String sessionId = request.getSession().getId();
|
||||||
|
// SessionInformation sessionInfo =
|
||||||
|
// sessionPersistentRegistry.getSessionInformation(sessionId);
|
||||||
|
//
|
||||||
|
// if (sessionInfo != null && sessionInfo.isExpired()) {
|
||||||
|
// SecurityContextHolder.clearContext();
|
||||||
|
// response.sendRedirect(request.getContextPath() + "/login?expired=true");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Check for API key in the request headers if no authentication exists
|
// Check for API key in the request headers if no authentication exists
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
String apiKey = request.getHeader("X-API-Key");
|
String apiKey = request.getHeader("X-API-Key");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||||
import stirling.software.SPDF.utils.FileInfo;
|
import stirling.software.SPDF.utils.FileInfo;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.opensaml.saml.saml2.core.Assertion;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class ConvertResponseToAuthentication
|
||||||
|
implements Converter<ResponseToken, Saml2Authentication> {
|
||||||
|
|
||||||
|
private final Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup;
|
||||||
|
|
||||||
|
public ConvertResponseToAuthentication(
|
||||||
|
Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup) {
|
||||||
|
this.saml2AuthorityAttributeLookup = saml2AuthorityAttributeLookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||||
|
final Assertion assertion =
|
||||||
|
CollectionUtils.firstElement(responseToken.getResponse().getAssertions());
|
||||||
|
final Map<String, List<Object>> attributes =
|
||||||
|
SamlAssertionUtils.getAssertionAttributes(assertion);
|
||||||
|
final String registrationId =
|
||||||
|
responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
|
||||||
|
final ScimSaml2AuthenticatedPrincipal principal =
|
||||||
|
new ScimSaml2AuthenticatedPrincipal(
|
||||||
|
assertion,
|
||||||
|
attributes,
|
||||||
|
saml2AuthorityAttributeLookup.getIdentityMappings(registrationId));
|
||||||
|
final Collection<? extends GrantedAuthority> assertionAuthorities =
|
||||||
|
getAssertionAuthorities(
|
||||||
|
attributes,
|
||||||
|
saml2AuthorityAttributeLookup.getAuthorityAttribute(registrationId));
|
||||||
|
return new Saml2Authentication(
|
||||||
|
principal, responseToken.getToken().getSaml2Response(), assertionAuthorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection<? extends GrantedAuthority> getAssertionAuthorities(
|
||||||
|
final Map<String, List<Object>> attributes, final String authoritiesAttributeName) {
|
||||||
|
if (attributes == null || attributes.isEmpty()) {
|
||||||
|
return Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Object> groups = new ArrayList<>();
|
||||||
|
|
||||||
|
if (attributes.get(authoritiesAttributeName) != null) {
|
||||||
|
groups = new ArrayList<>(attributes.get(authoritiesAttributeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.stream()
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast)
|
||||||
|
.map(String::toLowerCase)
|
||||||
|
.map(SimpleGrantedAuthority::new)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.DisabledException;
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
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
|
||||||
|
public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationFailure(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
AuthenticationException exception)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
if (exception instanceof BadCredentialsException) {
|
||||||
|
log.error("BadCredentialsException", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof DisabledException) {
|
||||||
|
log.error("User is deactivated: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof LockedException) {
|
||||||
|
log.error("Account locked: ", exception);
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (exception instanceof Saml2AuthenticationException) {
|
||||||
|
log.error("SAML2 Authentication error: ", exception);
|
||||||
|
getRedirectStrategy()
|
||||||
|
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.error("Unhandled authentication exception", exception);
|
||||||
|
super.onAuthenticationFailure(request, response, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.LockedException;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
|
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
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.AuthenticationType;
|
||||||
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CustomSAMLAuthenticationSuccessHandler
|
||||||
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
|
private LoginAttemptService loginAttemptService;
|
||||||
|
private UserService userService;
|
||||||
|
private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
public CustomSAMLAuthenticationSuccessHandler(
|
||||||
|
LoginAttemptService loginAttemptService,
|
||||||
|
UserService userService,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.loginAttemptService = loginAttemptService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAuthenticationSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
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();
|
||||||
|
} else if (principal instanceof ScimSaml2AuthenticatedPrincipal) {
|
||||||
|
ScimSaml2AuthenticatedPrincipal samlPrincipal =
|
||||||
|
(ScimSaml2AuthenticatedPrincipal) principal;
|
||||||
|
username = samlPrincipal.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the saved request
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
String contextPath = request.getContextPath();
|
||||||
|
SavedRequest savedRequest =
|
||||||
|
(session != null)
|
||||||
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (savedRequest != null
|
||||||
|
&& !RequestUriUtils.isStaticResource(contextPath, savedRequest.getRedirectUrl())) {
|
||||||
|
// Redirect to the original destination
|
||||||
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
|
} else {
|
||||||
|
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
|
||||||
|
|
||||||
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
if (session != null) {
|
||||||
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
|
}
|
||||||
|
throw new LockedException(
|
||||||
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
|
}
|
||||||
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
|
&& userService.hasPassword(username)
|
||||||
|
&& !userService.isAuthenticationTypeByUsername(
|
||||||
|
username, AuthenticationType.OAUTH2)
|
||||||
|
&& oAuth.getAutoCreateUser()) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (oAuth.getBlockRegistration()
|
||||||
|
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (principal instanceof OAuth2User) {
|
||||||
|
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||||
|
}
|
||||||
|
response.sendRedirect(contextPath + "/");
|
||||||
|
return;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
|
||||||
|
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLogoutSuccess(
|
||||||
|
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
|
||||||
|
String redirectUrl = determineTargetUrl(request, response, authentication);
|
||||||
|
|
||||||
|
if (response.isCommitted()) {
|
||||||
|
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String determineTargetUrl(
|
||||||
|
HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Authentication authentication) {
|
||||||
|
// Default to the root URL
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
public interface Saml2AuthorityAttributeLookup {
|
||||||
|
String getAuthorityAttribute(String registrationId);
|
||||||
|
|
||||||
|
SimpleScimMappings getIdentityMappings(String registrationId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class Saml2AuthorityAttributeLookupImpl implements Saml2AuthorityAttributeLookup {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAuthorityAttribute(String registrationId) {
|
||||||
|
return "authorityAttributeName";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimpleScimMappings getIdentityMappings(String registrationId) {
|
||||||
|
return new SimpleScimMappings();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.opensaml.core.xml.XMLObject;
|
||||||
|
import org.opensaml.core.xml.schema.*;
|
||||||
|
import org.opensaml.saml.saml2.core.Assertion;
|
||||||
|
|
||||||
|
public class SamlAssertionUtils {
|
||||||
|
|
||||||
|
public static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
|
||||||
|
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
assertion
|
||||||
|
.getAttributeStatements()
|
||||||
|
.forEach(
|
||||||
|
attributeStatement -> {
|
||||||
|
attributeStatement
|
||||||
|
.getAttributes()
|
||||||
|
.forEach(
|
||||||
|
attribute -> {
|
||||||
|
List<Object> attributeValues = new ArrayList<>();
|
||||||
|
|
||||||
|
attribute
|
||||||
|
.getAttributeValues()
|
||||||
|
.forEach(
|
||||||
|
xmlObject -> {
|
||||||
|
Object attributeValue =
|
||||||
|
getXmlObjectValue(
|
||||||
|
xmlObject);
|
||||||
|
if (attributeValue != null) {
|
||||||
|
attributeValues.add(
|
||||||
|
attributeValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
attributeMap.put(
|
||||||
|
attribute.getName(), attributeValues);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return attributeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getXmlObjectValue(XMLObject xmlObject) {
|
||||||
|
if (xmlObject instanceof XSAny) {
|
||||||
|
return ((XSAny) xmlObject).getTextContent();
|
||||||
|
} else if (xmlObject instanceof XSString) {
|
||||||
|
return ((XSString) xmlObject).getValue();
|
||||||
|
} else if (xmlObject instanceof XSInteger) {
|
||||||
|
return ((XSInteger) xmlObject).getValue();
|
||||||
|
} else if (xmlObject instanceof XSURI) {
|
||||||
|
return ((XSURI) xmlObject).getURI();
|
||||||
|
} else if (xmlObject instanceof XSBoolean) {
|
||||||
|
return ((XSBoolean) xmlObject).getValue().getValue();
|
||||||
|
} else if (xmlObject instanceof XSDateTime) {
|
||||||
|
Instant dateTime = ((XSDateTime) xmlObject).getValue();
|
||||||
|
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.toEpochMilli()) : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
|
||||||
|
import org.opensaml.security.x509.X509Support;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.security.converter.RsaKeyConverters;
|
||||||
|
import org.springframework.security.saml2.core.Saml2X509Credential;
|
||||||
|
import org.springframework.security.saml2.provider.service.registration.*;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
|
public class SamlConfig {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(
|
||||||
|
value = "security.saml.enabled",
|
||||||
|
havingValue = "true",
|
||||||
|
matchIfMissing = false)
|
||||||
|
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository()
|
||||||
|
throws CertificateException, IOException {
|
||||||
|
|
||||||
|
// Resource signingCertResource = new ClassPathResource(this.rpSigningCertLocation);
|
||||||
|
Resource signingCertResource =
|
||||||
|
resourceLoader.getResource(
|
||||||
|
this.applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getSaml()
|
||||||
|
.getCertificateLocation());
|
||||||
|
// Resource signingKeyResource = new ClassPathResource(this.rpSigningKeyLocation);
|
||||||
|
Resource signingKeyResource =
|
||||||
|
resourceLoader.getResource(
|
||||||
|
this.applicationProperties.getSecurity().getSaml().getPrivateKeyLocation());
|
||||||
|
try (InputStream is = signingKeyResource.getInputStream();
|
||||||
|
InputStream certIS = signingCertResource.getInputStream(); ) {
|
||||||
|
X509Certificate rpCertificate = X509Support.decodeCertificate(certIS.readAllBytes());
|
||||||
|
RSAPrivateKey rpKey = RsaKeyConverters.pkcs8().convert(is);
|
||||||
|
final Saml2X509Credential rpSigningCredentials =
|
||||||
|
Saml2X509Credential.signing(rpKey, rpCertificate);
|
||||||
|
|
||||||
|
X509Certificate apCert =
|
||||||
|
X509Support.decodeCertificate(
|
||||||
|
applicationProperties.getSecurity().getSaml().getSigningCertificate());
|
||||||
|
Saml2X509Credential apCredential = Saml2X509Credential.verification(apCert);
|
||||||
|
|
||||||
|
RelyingPartyRegistration registration =
|
||||||
|
RelyingPartyRegistrations.fromMetadataLocation(
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getSaml()
|
||||||
|
.getIdpMetadataLocation())
|
||||||
|
.entityId(applicationProperties.getSecurity().getSaml().getEntityId())
|
||||||
|
.registrationId(
|
||||||
|
applicationProperties
|
||||||
|
.getSecurity()
|
||||||
|
.getSaml()
|
||||||
|
.getRegistrationId())
|
||||||
|
.signingX509Credentials(c -> c.add(rpSigningCredentials))
|
||||||
|
.assertingPartyDetails(
|
||||||
|
party ->
|
||||||
|
party.wantAuthnRequestsSigned(true)
|
||||||
|
.verificationX509Credentials(
|
||||||
|
c -> c.add(apCredential)))
|
||||||
|
.singleLogoutServiceLocation("http://localhost:8090/logout/saml2/slo")
|
||||||
|
.build();
|
||||||
|
return new InMemoryRelyingPartyRegistrationRepository(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.opensaml.saml.saml2.core.Assertion;
|
||||||
|
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import com.unboundid.scim2.common.types.Email;
|
||||||
|
import com.unboundid.scim2.common.types.Name;
|
||||||
|
import com.unboundid.scim2.common.types.UserResource;
|
||||||
|
|
||||||
|
public class ScimSaml2AuthenticatedPrincipal implements AuthenticatedPrincipal, Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final transient UserResource userResource;
|
||||||
|
|
||||||
|
public ScimSaml2AuthenticatedPrincipal(
|
||||||
|
final Assertion assertion,
|
||||||
|
final Map<String, List<Object>> attributes,
|
||||||
|
final SimpleScimMappings attributeMappings) {
|
||||||
|
Assert.notNull(assertion, "assertion cannot be null");
|
||||||
|
Assert.notNull(assertion.getSubject(), "assertion subject cannot be null");
|
||||||
|
Assert.notNull(
|
||||||
|
assertion.getSubject().getNameID(), "assertion subject NameID cannot be null");
|
||||||
|
Assert.notNull(attributes, "attributes cannot be null");
|
||||||
|
Assert.notNull(attributeMappings, "attributeMappings cannot be null");
|
||||||
|
|
||||||
|
final Name name =
|
||||||
|
new Name()
|
||||||
|
.setFamilyName(
|
||||||
|
getAttribute(
|
||||||
|
attributes,
|
||||||
|
attributeMappings,
|
||||||
|
SimpleScimMappings::getFamilyName))
|
||||||
|
.setGivenName(
|
||||||
|
getAttribute(
|
||||||
|
attributes,
|
||||||
|
attributeMappings,
|
||||||
|
SimpleScimMappings::getGivenName));
|
||||||
|
|
||||||
|
final List<Email> emails = new ArrayList<>(1);
|
||||||
|
emails.add(
|
||||||
|
new Email()
|
||||||
|
.setValue(
|
||||||
|
getAttribute(
|
||||||
|
attributes,
|
||||||
|
attributeMappings,
|
||||||
|
SimpleScimMappings::getEmail))
|
||||||
|
.setPrimary(true));
|
||||||
|
|
||||||
|
userResource =
|
||||||
|
new UserResource()
|
||||||
|
.setUserName(assertion.getSubject().getNameID().getValue())
|
||||||
|
.setName(name)
|
||||||
|
.setEmails(emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getAttribute(
|
||||||
|
final Map<String, List<Object>> attributes,
|
||||||
|
final SimpleScimMappings simpleScimMappings,
|
||||||
|
final Function<SimpleScimMappings, String> attributeMapper) {
|
||||||
|
|
||||||
|
final String key = attributeMapper.apply(simpleScimMappings);
|
||||||
|
|
||||||
|
final List<Object> values = attributes.getOrDefault(key, Collections.emptyList());
|
||||||
|
|
||||||
|
return values.stream()
|
||||||
|
.filter(String.class::isInstance)
|
||||||
|
.map(String.class::cast)
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return this.userResource.getUserName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserResource getUserResource() {
|
||||||
|
return this.userResource;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SimpleScimMappings {
|
||||||
|
String givenName;
|
||||||
|
String familyName;
|
||||||
|
String email;
|
||||||
|
}
|
||||||
@@ -11,16 +11,19 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class CustomHttpSessionListener implements HttpSessionListener {
|
public class CustomHttpSessionListener implements HttpSessionListener {
|
||||||
|
|
||||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
private SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) {
|
||||||
|
super();
|
||||||
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionCreated(HttpSessionEvent se) {
|
public void sessionCreated(HttpSessionEvent se) {}
|
||||||
log.info("Session created: " + se.getSession().getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sessionDestroyed(HttpSessionEvent se) {
|
public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
log.info("Session destroyed: " + se.getSession().getId());
|
|
||||||
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
|
// Clear old sessions for the principal (unsure if needed)
|
||||||
|
// List<SessionEntity> existingSessions =
|
||||||
|
// sessionRepository.findByPrincipalName(principalName);
|
||||||
|
// for (SessionEntity session : existingSessions) {
|
||||||
|
// session.setExpired(true);
|
||||||
|
// sessionRepository.save(session);
|
||||||
|
// }
|
||||||
|
|
||||||
SessionEntity sessionEntity = new SessionEntity();
|
SessionEntity sessionEntity = new SessionEntity();
|
||||||
sessionEntity.setSessionId(sessionId);
|
sessionEntity.setSessionId(sessionId);
|
||||||
sessionEntity.setPrincipalName(principalName);
|
sessionEntity.setPrincipalName(principalName);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
import stirling.software.SPDF.service.PostHogService;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -36,9 +37,13 @@ public class CropController {
|
|||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
private final PostHogService postHogService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public CropController(CustomPDDocumentFactory pdfDocumentFactory) {
|
public CropController(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) {
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.postHogService = postHogService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
@PostMapping(value = "/crop", consumes = "multipart/form-data")
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
@Tag(name = "Settings", description = "Settings APIs")
|
||||||
|
@RequestMapping("/api/v1/settings")
|
||||||
|
@Hidden
|
||||||
|
public class SettingsController {
|
||||||
|
|
||||||
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@PostMapping("/update-enable-analytics")
|
||||||
|
@Hidden
|
||||||
|
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||||
|
if (!"undefined".equals(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
|
||||||
|
.body(
|
||||||
|
"Setting has already been set, To adjust please edit /config/settings.yml");
|
||||||
|
}
|
||||||
|
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
|
||||||
|
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
|
||||||
|
return ResponseEntity.ok("Updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,8 +60,6 @@ public class SplitPDFController {
|
|||||||
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
||||||
int totalPages = document.getNumberOfPages();
|
int totalPages = document.getNumberOfPages();
|
||||||
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
|
||||||
System.out.println(
|
|
||||||
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
|
||||||
if (!pageNumbers.contains(totalPages - 1)) {
|
if (!pageNumbers.contains(totalPages - 1)) {
|
||||||
// Create a mutable ArrayList so we can add to it
|
// Create a mutable ArrayList so we can add to it
|
||||||
pageNumbers = new ArrayList<>(pageNumbers);
|
pageNumbers = new ArrayList<>(pageNumbers);
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import stirling.software.SPDF.config.PdfMetadataService;
|
|
||||||
import stirling.software.SPDF.model.PdfMetadata;
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||||
|
import stirling.software.SPDF.service.PdfMetadataService;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
@@ -40,6 +41,7 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
|
|||||||
@Controller
|
@Controller
|
||||||
@Tag(name = "User", description = "User APIs")
|
@Tag(name = "User", description = "User APIs")
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
|
@Slf4j
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
@Autowired private UserService userService;
|
@Autowired private UserService userService;
|
||||||
@@ -191,13 +193,11 @@ public class UserController {
|
|||||||
Map<String, String[]> paramMap = request.getParameterMap();
|
Map<String, String[]> paramMap = request.getParameterMap();
|
||||||
Map<String, String> updates = new HashMap<>();
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
|
||||||
System.out.println("Received parameter map: " + paramMap);
|
|
||||||
|
|
||||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||||
updates.put(entry.getKey(), entry.getValue()[0]);
|
updates.put(entry.getKey(), entry.getValue()[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Processed updates: " + updates);
|
log.debug("Processed updates: " + updates);
|
||||||
|
|
||||||
// Assuming you have a method in userService to update the settings for a user
|
// Assuming you have a method in userService to update the settings for a user
|
||||||
userService.updateUserSettings(principal.getName(), updates);
|
userService.updateUserSettings(principal.getName(), updates);
|
||||||
@@ -209,7 +209,7 @@ public class UserController {
|
|||||||
@PostMapping("/admin/saveUser")
|
@PostMapping("/admin/saveUser")
|
||||||
public RedirectView saveUser(
|
public RedirectView saveUser(
|
||||||
@RequestParam(name = "username", required = true) String username,
|
@RequestParam(name = "username", required = true) String username,
|
||||||
@RequestParam(name = "password", required = true) String password,
|
@RequestParam(name = "password", required = false) String password,
|
||||||
@RequestParam(name = "role") String role,
|
@RequestParam(name = "role") String role,
|
||||||
@RequestParam(name = "authType") String authType,
|
@RequestParam(name = "authType") String authType,
|
||||||
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ public class ConvertPDFToPDFA {
|
|||||||
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
|
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
|
||||||
command.add("-dNOPAUSE");
|
command.add("-dNOPAUSE");
|
||||||
command.add("-dBATCH");
|
command.add("-dBATCH");
|
||||||
command.add("-sColorConversionStrategy=UseDeviceIndependentColor");
|
command.add("-sColorConversionStrategy=sRGB");
|
||||||
command.add("-sDEVICE=pdfwrite");
|
command.add("-sDEVICE=pdfwrite");
|
||||||
command.add("-dPDFACompatibilityPolicy=2");
|
command.add("-dPDFACompatibilityPolicy=2");
|
||||||
command.add("-o");
|
command.add("-o");
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ public class ExtractImagesController {
|
|||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
String format = request.getFormat();
|
String format = request.getFormat();
|
||||||
boolean allowDuplicates = request.isAllowDuplicates();
|
boolean allowDuplicates = request.isAllowDuplicates();
|
||||||
System.out.println(
|
|
||||||
System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format);
|
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|
||||||
// Determine if multithreading should be used based on PDF size or number of pages
|
// Determine if multithreading should be used based on PDF size or number of pages
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
import stirling.software.SPDF.model.api.misc.PrintFileRequest;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
@Slf4j
|
||||||
public class PrintFileController {
|
public class PrintFileController {
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -59,7 +61,7 @@ public class PrintFileController {
|
|||||||
new IllegalArgumentException(
|
new IllegalArgumentException(
|
||||||
"No matching printer found"));
|
"No matching printer found"));
|
||||||
|
|
||||||
System.out.println("Selected Printer: " + selectedService.getName());
|
log.info("Selected Printer: " + selectedService.getName());
|
||||||
|
|
||||||
if ("application/pdf".equals(contentType)) {
|
if ("application/pdf".equals(contentType)) {
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = Loader.loadPDF(file.getBytes());
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ public class RedactController {
|
|||||||
float customPadding = request.getCustomPadding();
|
float customPadding = request.getCustomPadding();
|
||||||
boolean convertPDFToImage = request.isConvertPDFToImage();
|
boolean convertPDFToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
System.out.println(listOfTextString);
|
|
||||||
String[] listOfText = listOfTextString.split("\n");
|
String[] listOfText = listOfTextString.split("\n");
|
||||||
PDDocument document = pdfDocumentFactory.load(file);
|
PDDocument document = pdfDocumentFactory.load(file);
|
||||||
|
|
||||||
@@ -75,7 +74,6 @@ public class RedactController {
|
|||||||
|
|
||||||
for (String text : listOfText) {
|
for (String text : listOfText) {
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
System.out.println(text);
|
|
||||||
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
|
||||||
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
List<PDFText> foundTexts = textFinder.getTextLocations(document);
|
||||||
redactFoundText(document, foundTexts, customPadding, redactColor);
|
redactFoundText(document, foundTexts, customPadding, redactColor);
|
||||||
|
|||||||
@@ -108,6 +108,13 @@ public class GeneralWebController {
|
|||||||
return "split-pdf-by-sections";
|
return "split-pdf-by-sections";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/split-pdf-by-chapters")
|
||||||
|
@Hidden
|
||||||
|
public String splitPdfByChapters(Model model) {
|
||||||
|
model.addAttribute("currentPage", "split-pdf-by-chapters");
|
||||||
|
return "split-pdf-by-chapters";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/view-pdf")
|
@GetMapping("/view-pdf")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String ViewPdfForm2(Model model) {
|
public String ViewPdfForm2(Model model) {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.PropertySource;
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
@@ -24,6 +26,7 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|||||||
@ConfigurationProperties(prefix = "")
|
@ConfigurationProperties(prefix = "")
|
||||||
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
|
||||||
@Data
|
@Data
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
public class ApplicationProperties {
|
public class ApplicationProperties {
|
||||||
|
|
||||||
private Legal legal = new Legal();
|
private Legal legal = new Legal();
|
||||||
@@ -57,6 +60,7 @@ public class ApplicationProperties {
|
|||||||
private Boolean csrfDisabled;
|
private Boolean csrfDisabled;
|
||||||
private InitialLogin initialLogin = new InitialLogin();
|
private InitialLogin initialLogin = new InitialLogin();
|
||||||
private OAUTH2 oauth2 = new OAUTH2();
|
private OAUTH2 oauth2 = new OAUTH2();
|
||||||
|
private SAML saml = new SAML();
|
||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
private String loginMethod = "all";
|
private String loginMethod = "all";
|
||||||
@@ -67,6 +71,39 @@ public class ApplicationProperties {
|
|||||||
@ToString.Exclude private String password;
|
@ToString.Exclude private String password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class SAML {
|
||||||
|
private Boolean enabled = false;
|
||||||
|
private String entityId;
|
||||||
|
private String registrationId;
|
||||||
|
private String spBaseUrl;
|
||||||
|
private String idpMetadataLocation;
|
||||||
|
// private KeyStore keystore;
|
||||||
|
private String privateKeyLocation;
|
||||||
|
private String certificateLocation;
|
||||||
|
private String singleLogoutBinding;
|
||||||
|
private String singleLogoutResponseUri;
|
||||||
|
private String signingCertificate;
|
||||||
|
|
||||||
|
// @Data
|
||||||
|
// public static class KeyStore {
|
||||||
|
// private String keystoreLocation;
|
||||||
|
// private String keystorePassword;
|
||||||
|
// private String keyAlias;
|
||||||
|
// private String keyPassword;
|
||||||
|
// private String realmCertificateAlias;
|
||||||
|
//
|
||||||
|
// public Resource getKeystoreResource() {
|
||||||
|
// if (keystoreLocation.startsWith("classpath:")) {
|
||||||
|
// return new ClassPathResource(
|
||||||
|
// keystoreLocation.substring("classpath:".length()));
|
||||||
|
// } else {
|
||||||
|
// return new FileSystemResource(keystoreLocation);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class OAUTH2 {
|
public static class OAUTH2 {
|
||||||
private Boolean enabled = false;
|
private Boolean enabled = false;
|
||||||
@@ -136,6 +173,7 @@ public class ApplicationProperties {
|
|||||||
private boolean customHTMLFiles;
|
private boolean customHTMLFiles;
|
||||||
private String tessdataDir;
|
private String tessdataDir;
|
||||||
private Boolean enableAlphaFunctionality;
|
private Boolean enableAlphaFunctionality;
|
||||||
|
private String enableAnalytics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -175,10 +213,12 @@ public class ApplicationProperties {
|
|||||||
@Data
|
@Data
|
||||||
public static class AutomaticallyGenerated {
|
public static class AutomaticallyGenerated {
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
|
private String UUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class EnterpriseEdition {
|
public static class EnterpriseEdition {
|
||||||
|
private boolean enabled;
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
private CustomMetadata customMetadata = new CustomMetadata();
|
private CustomMetadata customMetadata = new CustomMetadata();
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
|||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
import org.apache.pdfbox.text.PDFTextStripper;
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
import org.apache.pdfbox.text.TextPosition;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.PDFText;
|
import stirling.software.SPDF.model.PDFText;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class TextFinder extends PDFTextStripper {
|
public class TextFinder extends PDFTextStripper {
|
||||||
|
|
||||||
private final String searchText;
|
private final String searchText;
|
||||||
@@ -92,7 +94,7 @@ public class TextFinder extends PDFTextStripper {
|
|||||||
|
|
||||||
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
|
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
|
||||||
this.getText(document);
|
this.getText(document);
|
||||||
System.out.println(
|
log.debug(
|
||||||
"Found "
|
"Found "
|
||||||
+ textOccurrences.size()
|
+ textOccurrences.size()
|
||||||
+ " occurrences of '"
|
+ " occurrences of '"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.PdfMetadataService;
|
|
||||||
import stirling.software.SPDF.model.PdfMetadata;
|
import stirling.software.SPDF.model.PdfMetadata;
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
import io.micrometer.core.instrument.search.Search;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class MetricsAggregatorService {
|
||||||
|
|
||||||
|
private final MeterRegistry meterRegistry;
|
||||||
|
private final PostHogService postHogService;
|
||||||
|
private final Map<String, Double> lastSentMetrics = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) {
|
||||||
|
this.meterRegistry = meterRegistry;
|
||||||
|
this.postHogService = postHogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 900000) // Run every 15 minutes
|
||||||
|
public void aggregateAndSendMetrics() {
|
||||||
|
Map<String, Object> metrics = new HashMap<>();
|
||||||
|
Search.in(meterRegistry)
|
||||||
|
.name("http.requests")
|
||||||
|
.counters()
|
||||||
|
.forEach(
|
||||||
|
counter -> {
|
||||||
|
String key =
|
||||||
|
String.format(
|
||||||
|
"http_requests_%s_%s",
|
||||||
|
counter.getId().getTag("method"),
|
||||||
|
counter.getId().getTag("uri").replace("/", "_"));
|
||||||
|
|
||||||
|
double currentCount = counter.count();
|
||||||
|
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||||
|
double difference = currentCount - lastCount;
|
||||||
|
|
||||||
|
if (difference > 0) {
|
||||||
|
metrics.put(key, difference);
|
||||||
|
lastSentMetrics.put(key, currentCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send aggregated metrics to PostHog
|
||||||
|
if (!metrics.isEmpty()) {
|
||||||
|
postHogService.captureEvent("aggregated_metrics", metrics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
@@ -15,16 +15,16 @@ import stirling.software.SPDF.model.PdfMetadata;
|
|||||||
public class PdfMetadataService {
|
public class PdfMetadataService {
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final String appVersion;
|
private final String stirlingPDFLabel;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PdfMetadataService(
|
public PdfMetadataService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Qualifier("appVersion") String appVersion,
|
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
||||||
@Autowired(required = false) UserServiceInterface userService) {
|
@Autowired(required = false) UserServiceInterface userService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.appVersion = appVersion;
|
this.stirlingPDFLabel = stirlingPDFLabel;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,51 +59,40 @@ public class PdfMetadataService {
|
|||||||
|
|
||||||
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setNewDocumentMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
|
|
||||||
String creator = "Stirling-PDF";
|
String creator = stirlingPDFLabel;
|
||||||
|
|
||||||
// if (applicationProperties
|
if (applicationProperties
|
||||||
// .getEnterpriseEdition()
|
.getEnterpriseEdition()
|
||||||
// .getCustomMetadata()
|
.getCustomMetadata()
|
||||||
// .isAutoUpdateMetadata()) {
|
.isAutoUpdateMetadata()) {
|
||||||
|
|
||||||
// producer =
|
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
||||||
//
|
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||||
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer();
|
}
|
||||||
// creator =
|
|
||||||
// applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
|
||||||
// title = applicationProperties.getEnterpriseEdition().getCustomMetadata().getTitle();
|
|
||||||
|
|
||||||
// if ("{filename}".equals(title)) {
|
pdf.getDocumentInformation().setCreator(creator);
|
||||||
// title = "Filename"; // Replace with actual filename logic
|
|
||||||
// } else if ("{unchanged}".equals(title)) {
|
|
||||||
// title = pdfMetadata.getTitle(); // Keep the original title
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pdf.getDocumentInformation().setCreator(creator + " " + appVersion);
|
|
||||||
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
pdf.getDocumentInformation().setCreationDate(Calendar.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
private void setCommonMetadata(PDDocument pdf, PdfMetadata pdfMetadata) {
|
||||||
String producer = "Stirling-PDF";
|
|
||||||
String title = pdfMetadata.getTitle();
|
String title = pdfMetadata.getTitle();
|
||||||
pdf.getDocumentInformation().setTitle(title);
|
pdf.getDocumentInformation().setTitle(title);
|
||||||
pdf.getDocumentInformation().setProducer(producer + " " + appVersion);
|
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||||
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
pdf.getDocumentInformation().setSubject(pdfMetadata.getSubject());
|
||||||
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
pdf.getDocumentInformation().setKeywords(pdfMetadata.getKeywords());
|
||||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||||
|
|
||||||
String author = pdfMetadata.getAuthor();
|
String author = pdfMetadata.getAuthor();
|
||||||
// if (applicationProperties
|
if (applicationProperties
|
||||||
// .getEnterpriseEdition()
|
.getEnterpriseEdition()
|
||||||
// .getCustomMetadata()
|
.getCustomMetadata()
|
||||||
// .isAutoUpdateMetadata()) {
|
.isAutoUpdateMetadata()) {
|
||||||
// author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
||||||
|
|
||||||
// if (userService != null) {
|
if (userService != null) {
|
||||||
// author = author.replace("username", userService.getCurrentUsername());
|
author = author.replace("username", userService.getCurrentUsername());
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
pdf.getDocumentInformation().setAuthor(author);
|
pdf.getDocumentInformation().setAuthor(author);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
379
src/main/java/stirling/software/SPDF/service/PostHogService.java
Normal file
379
src/main/java/stirling/software/SPDF/service/PostHogService.java
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.management.*;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TimeZone;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.posthog.java.PostHog;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PostHogService {
|
||||||
|
private final PostHog postHog;
|
||||||
|
private final String uniqueId;
|
||||||
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public PostHogService(
|
||||||
|
PostHog postHog,
|
||||||
|
@Qualifier("UUID") String uuid,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
|
this.postHog = postHog;
|
||||||
|
this.uniqueId = uuid;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
|
captureSystemInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void captureSystemInfo() {
|
||||||
|
if (!Boolean.getBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
postHog.capture(uniqueId, "system_info_captured", captureServerMetrics());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Handle exceptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void captureEvent(String eventName, Map<String, Object> properties) {
|
||||||
|
if (!Boolean.getBoolean(applicationProperties.getSystem().getEnableAnalytics())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
postHog.capture(uniqueId, eventName, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> captureServerMetrics() {
|
||||||
|
Map<String, Object> metrics = new HashMap<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// System info
|
||||||
|
metrics.put("os_name", System.getProperty("os.name"));
|
||||||
|
metrics.put("os_version", System.getProperty("os.version"));
|
||||||
|
metrics.put("java_version", System.getProperty("java.version"));
|
||||||
|
metrics.put("user_name", System.getProperty("user.name"));
|
||||||
|
metrics.put("user_home", System.getProperty("user.home"));
|
||||||
|
metrics.put("user_dir", System.getProperty("user.dir"));
|
||||||
|
|
||||||
|
// CPU and Memory
|
||||||
|
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
|
||||||
|
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
|
||||||
|
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
|
||||||
|
|
||||||
|
// Network and Server Identity
|
||||||
|
InetAddress localHost = InetAddress.getLocalHost();
|
||||||
|
metrics.put("ip_address", localHost.getHostAddress());
|
||||||
|
metrics.put("hostname", localHost.getHostName());
|
||||||
|
metrics.put("mac_address", getMacAddress());
|
||||||
|
|
||||||
|
// JVM info
|
||||||
|
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
|
||||||
|
metrics.put("jvm_version", System.getProperty("java.vm.version"));
|
||||||
|
|
||||||
|
// Locale and Timezone
|
||||||
|
metrics.put("system_language", System.getProperty("user.language"));
|
||||||
|
metrics.put("system_country", System.getProperty("user.country"));
|
||||||
|
metrics.put("timezone", TimeZone.getDefault().getID());
|
||||||
|
metrics.put("locale", Locale.getDefault().toString());
|
||||||
|
|
||||||
|
// Disk info
|
||||||
|
File root = new File(".");
|
||||||
|
metrics.put("total_disk_space", root.getTotalSpace());
|
||||||
|
metrics.put("free_disk_space", root.getFreeSpace());
|
||||||
|
|
||||||
|
// Process info
|
||||||
|
metrics.put("process_id", ProcessHandle.current().pid());
|
||||||
|
|
||||||
|
// JVM metrics
|
||||||
|
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
|
||||||
|
metrics.put("jvm_uptime_ms", runtimeMXBean.getUptime());
|
||||||
|
metrics.put("jvm_start_time", runtimeMXBean.getStartTime());
|
||||||
|
|
||||||
|
// Memory metrics
|
||||||
|
MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean();
|
||||||
|
metrics.put("heap_memory_usage", memoryMXBean.getHeapMemoryUsage().getUsed());
|
||||||
|
metrics.put("non_heap_memory_usage", memoryMXBean.getNonHeapMemoryUsage().getUsed());
|
||||||
|
|
||||||
|
// CPU metrics
|
||||||
|
OperatingSystemMXBean osMXBean = ManagementFactory.getOperatingSystemMXBean();
|
||||||
|
metrics.put("system_load_average", osMXBean.getSystemLoadAverage());
|
||||||
|
|
||||||
|
// Thread metrics
|
||||||
|
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
|
||||||
|
metrics.put("thread_count", threadMXBean.getThreadCount());
|
||||||
|
metrics.put("daemon_thread_count", threadMXBean.getDaemonThreadCount());
|
||||||
|
metrics.put("peak_thread_count", threadMXBean.getPeakThreadCount());
|
||||||
|
|
||||||
|
// Garbage collection metrics
|
||||||
|
for (GarbageCollectorMXBean gcBean : ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||||
|
metrics.put("gc_" + gcBean.getName() + "_count", gcBean.getCollectionCount());
|
||||||
|
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network interfaces
|
||||||
|
metrics.put("network_interfaces", getNetworkInterfacesInfo());
|
||||||
|
|
||||||
|
// Docker detection and stats
|
||||||
|
boolean isDocker = isRunningInDocker();
|
||||||
|
metrics.put("is_docker", isDocker);
|
||||||
|
if (isDocker) {
|
||||||
|
metrics.put("docker_metrics", getDockerMetrics());
|
||||||
|
}
|
||||||
|
metrics.put("application_properties", captureApplicationProperties());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
metrics.put("error", e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRunningInDocker() {
|
||||||
|
return Files.exists(Paths.get("/.dockerenv"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> getDockerMetrics() {
|
||||||
|
Map<String, Object> dockerMetrics = new HashMap<>();
|
||||||
|
|
||||||
|
// Network-related Docker info
|
||||||
|
dockerMetrics.put("docker_network_mode", System.getenv("DOCKER_NETWORK_MODE"));
|
||||||
|
|
||||||
|
// Container name (if set)
|
||||||
|
String containerName = System.getenv("CONTAINER_NAME");
|
||||||
|
if (containerName != null && !containerName.isEmpty()) {
|
||||||
|
dockerMetrics.put("container_name", containerName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Docker compose information
|
||||||
|
String composeProject = System.getenv("COMPOSE_PROJECT_NAME");
|
||||||
|
String composeService = System.getenv("COMPOSE_SERVICE_NAME");
|
||||||
|
if (composeProject != null && composeService != null) {
|
||||||
|
dockerMetrics.put("compose_project", composeProject);
|
||||||
|
dockerMetrics.put("compose_service", composeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kubernetes-specific info (if running in K8s)
|
||||||
|
String k8sPodName = System.getenv("KUBERNETES_POD_NAME");
|
||||||
|
if (k8sPodName != null) {
|
||||||
|
dockerMetrics.put("k8s_pod_name", k8sPodName);
|
||||||
|
dockerMetrics.put("k8s_namespace", System.getenv("KUBERNETES_NAMESPACE"));
|
||||||
|
dockerMetrics.put("k8s_node_name", System.getenv("KUBERNETES_NODE_NAME"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// New environment variables
|
||||||
|
dockerMetrics.put("version_tag", System.getenv("VERSION_TAG"));
|
||||||
|
dockerMetrics.put("docker_enable_security", System.getenv("DOCKER_ENABLE_SECURITY"));
|
||||||
|
dockerMetrics.put("fat_docker", System.getenv("FAT_DOCKER"));
|
||||||
|
|
||||||
|
return dockerMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addIfNotEmpty(Map<String, Object> map, String key, Object value) {
|
||||||
|
if (value != null) {
|
||||||
|
if (value instanceof String) {
|
||||||
|
String strValue = (String) value;
|
||||||
|
if (!StringUtils.isBlank(strValue)) {
|
||||||
|
map.put(key, strValue.trim());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
map.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> captureApplicationProperties() {
|
||||||
|
Map<String, Object> properties = new HashMap<>();
|
||||||
|
|
||||||
|
// Capture Legal properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"legal_termsAndConditions",
|
||||||
|
applicationProperties.getLegal().getTermsAndConditions());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"legal_privacyPolicy",
|
||||||
|
applicationProperties.getLegal().getPrivacyPolicy());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"legal_accessibilityStatement",
|
||||||
|
applicationProperties.getLegal().getAccessibilityStatement());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"legal_cookiePolicy",
|
||||||
|
applicationProperties.getLegal().getCookiePolicy());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties, "legal_impressum", applicationProperties.getLegal().getImpressum());
|
||||||
|
|
||||||
|
// Capture Security properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_enableLogin",
|
||||||
|
applicationProperties.getSecurity().getEnableLogin());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_csrfDisabled",
|
||||||
|
applicationProperties.getSecurity().getCsrfDisabled());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_loginAttemptCount",
|
||||||
|
applicationProperties.getSecurity().getLoginAttemptCount());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_loginResetTimeMinutes",
|
||||||
|
applicationProperties.getSecurity().getLoginResetTimeMinutes());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_loginMethod",
|
||||||
|
applicationProperties.getSecurity().getLoginMethod());
|
||||||
|
|
||||||
|
// Capture OAuth2 properties (excluding sensitive information)
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_oauth2_enabled",
|
||||||
|
applicationProperties.getSecurity().getOauth2().getEnabled());
|
||||||
|
if (applicationProperties.getSecurity().getOauth2().getEnabled()) {
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_oauth2_autoCreateUser",
|
||||||
|
applicationProperties.getSecurity().getOauth2().getAutoCreateUser());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_oauth2_blockRegistration",
|
||||||
|
applicationProperties.getSecurity().getOauth2().getBlockRegistration());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_oauth2_useAsUsername",
|
||||||
|
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"security_oauth2_provider",
|
||||||
|
applicationProperties.getSecurity().getOauth2().getProvider());
|
||||||
|
}
|
||||||
|
// Capture System properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_defaultLocale",
|
||||||
|
applicationProperties.getSystem().getDefaultLocale());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_googlevisibility",
|
||||||
|
applicationProperties.getSystem().getGooglevisibility());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_showUpdateOnlyAdmin",
|
||||||
|
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_customHTMLFiles",
|
||||||
|
applicationProperties.getSystem().isCustomHTMLFiles());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_tessdataDir",
|
||||||
|
applicationProperties.getSystem().getTessdataDir());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_enableAlphaFunctionality",
|
||||||
|
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"system_enableAnalytics",
|
||||||
|
applicationProperties.getSystem().getEnableAnalytics());
|
||||||
|
|
||||||
|
// Capture UI properties
|
||||||
|
addIfNotEmpty(properties, "ui_appName", applicationProperties.getUi().getAppName());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"ui_homeDescription",
|
||||||
|
applicationProperties.getUi().getHomeDescription());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties, "ui_appNameNavbar", applicationProperties.getUi().getAppNameNavbar());
|
||||||
|
|
||||||
|
// Capture Metrics properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
|
||||||
|
|
||||||
|
// Capture EnterpriseEdition properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"enterpriseEdition_enabled",
|
||||||
|
applicationProperties.getEnterpriseEdition().isEnabled());
|
||||||
|
if (applicationProperties.getEnterpriseEdition().isEnabled()) {
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"enterpriseEdition_customMetadata_autoUpdateMetadata",
|
||||||
|
applicationProperties
|
||||||
|
.getEnterpriseEdition()
|
||||||
|
.getCustomMetadata()
|
||||||
|
.isAutoUpdateMetadata());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"enterpriseEdition_customMetadata_author",
|
||||||
|
applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"enterpriseEdition_customMetadata_creator",
|
||||||
|
applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator());
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"enterpriseEdition_customMetadata_producer",
|
||||||
|
applicationProperties.getEnterpriseEdition().getCustomMetadata().getProducer());
|
||||||
|
}
|
||||||
|
// Capture AutoPipeline properties
|
||||||
|
addIfNotEmpty(
|
||||||
|
properties,
|
||||||
|
"autoPipeline_outputFolder",
|
||||||
|
applicationProperties.getAutoPipeline().getOutputFolder());
|
||||||
|
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getMacAddress() {
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> networkInterfaces =
|
||||||
|
NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (networkInterfaces.hasMoreElements()) {
|
||||||
|
NetworkInterface ni = networkInterfaces.nextElement();
|
||||||
|
byte[] hardwareAddress = ni.getHardwareAddress();
|
||||||
|
if (hardwareAddress != null) {
|
||||||
|
String[] hexadecimal = new String[hardwareAddress.length];
|
||||||
|
for (int i = 0; i < hardwareAddress.length; i++) {
|
||||||
|
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
|
||||||
|
}
|
||||||
|
return String.join("-", hexadecimal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Handle exception
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getNetworkInterfacesInfo() {
|
||||||
|
Map<String, String> interfacesInfo = new HashMap<>();
|
||||||
|
try {
|
||||||
|
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (nets.hasMoreElements()) {
|
||||||
|
NetworkInterface netint = nets.nextElement();
|
||||||
|
interfacesInfo.put(netint.getName(), netint.getDisplayName());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
interfacesInfo.put("error", e.getMessage());
|
||||||
|
}
|
||||||
|
return interfacesInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,18 +5,28 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.NetworkInterface;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileVisitResult;
|
import java.nio.file.FileVisitResult;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
import java.nio.file.SimpleFileVisitor;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.simpleyaml.configuration.file.YamlFile;
|
||||||
|
import org.simpleyaml.configuration.file.YamlFileWrapper;
|
||||||
|
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||||
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
@@ -262,4 +272,81 @@ public class GeneralUtils {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isValidUUID(String uuid) {
|
||||||
|
if (uuid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
UUID.fromString(uuid);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveKeyToConfig(String id, String key) throws IOException {
|
||||||
|
saveKeyToConfig(id, key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
||||||
|
throws IOException {
|
||||||
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
|
|
||||||
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
||||||
|
yamlOptionssettingsYml.setSplitLines(false);
|
||||||
|
|
||||||
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
YamlFileWrapper writer = settingsYml.path(id).set(key);
|
||||||
|
if (autoGenerated) {
|
||||||
|
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
|
}
|
||||||
|
settingsYml.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateMachineFingerprint() {
|
||||||
|
try {
|
||||||
|
// Get the MAC address
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
InetAddress ip = InetAddress.getLocalHost();
|
||||||
|
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
|
||||||
|
|
||||||
|
if (network == null) {
|
||||||
|
Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
|
||||||
|
while (networks.hasMoreElements()) {
|
||||||
|
NetworkInterface net = networks.nextElement();
|
||||||
|
byte[] mac = net.getHardwareAddress();
|
||||||
|
if (mac != null) {
|
||||||
|
for (int i = 0; i < mac.length; i++) {
|
||||||
|
sb.append(String.format("%02X", mac[i]));
|
||||||
|
}
|
||||||
|
break; // Use the first network interface with a MAC address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
byte[] mac = network.getHardwareAddress();
|
||||||
|
if (mac != null) {
|
||||||
|
for (int i = 0; i < mac.length; i++) {
|
||||||
|
sb.append(String.format("%02X", mac[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the MAC address for privacy and consistency
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
|
byte[] hash = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder fingerprint = new StringBuilder();
|
||||||
|
for (byte b : hash) {
|
||||||
|
fingerprint.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fingerprint.toString();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "GenericID";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,6 @@ public class PDFToFile {
|
|||||||
Files.deleteIfExists(tempInputFile);
|
Files.deleteIfExists(tempInputFile);
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||||
}
|
}
|
||||||
System.out.println("fileBytes=" + fileBytes.length);
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class RequestUriUtils {
|
|||||||
|| requestURI.startsWith(contextPath + "/public/")
|
|| requestURI.startsWith(contextPath + "/public/")
|
||||||
|| requestURI.startsWith(contextPath + "/pdfjs/")
|
|| requestURI.startsWith(contextPath + "/pdfjs/")
|
||||||
|| requestURI.startsWith(contextPath + "/login")
|
|| requestURI.startsWith(contextPath + "/login")
|
||||||
|
|| requestURI.startsWith(contextPath + "/error")
|
||||||
|| requestURI.endsWith(".svg")
|
|| requestURI.endsWith(".svg")
|
||||||
|| requestURI.endsWith(".png")
|
|| requestURI.endsWith(".png")
|
||||||
|| requestURI.endsWith(".ico")
|
|| requestURI.endsWith(".ico")
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import org.springframework.core.io.InputStreamResource;
|
|||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
import stirling.software.SPDF.model.api.PDFFile;
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
|
import stirling.software.SPDF.model.api.misc.ReplaceAndInvert;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
// @EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public abstract class ReplaceAndInvertColorStrategy extends PDFFile {
|
public abstract class ReplaceAndInvertColorStrategy extends PDFFile {
|
||||||
|
|
||||||
protected ReplaceAndInvert replaceAndInvert;
|
protected ReplaceAndInvert replaceAndInvert;
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
|||||||
|
|
||||||
spring.devtools.restart.enabled=true
|
spring.devtools.restart.enabled=true
|
||||||
spring.devtools.livereload.enabled=true
|
spring.devtools.livereload.enabled=true
|
||||||
|
|
||||||
spring.thymeleaf.encoding=UTF-8
|
spring.thymeleaf.encoding=UTF-8
|
||||||
|
|
||||||
|
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||||
|
|
||||||
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||||
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
||||||
@@ -41,7 +41,7 @@ spring.datasource.username=sa
|
|||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
spring.h2.console.enabled=false
|
spring.h2.console.enabled=false
|
||||||
spring.jpa.hibernate.ddl-auto=update
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
server.servlet.session.timeout: 30m
|
||||||
# Change the default URL path for OpenAPI JSON
|
# Change the default URL path for OpenAPI JSON
|
||||||
springdoc.api-docs.path=/v1/api-docs
|
springdoc.api-docs.path=/v1/api-docs
|
||||||
|
|
||||||
@@ -49,3 +49,5 @@ springdoc.api-docs.path=/v1/api-docs
|
|||||||
springdoc.swagger-ui.url=/v1/api-docs
|
springdoc.swagger-ui.url=/v1/api-docs
|
||||||
|
|
||||||
|
|
||||||
|
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
||||||
|
posthog.host=https://eu.i.posthog.com
|
||||||
@@ -382,7 +382,7 @@ home.scalePages.title=ضبط حجم/مقياس الصفحة
|
|||||||
home.scalePages.desc=تغيير حجم/مقياس الصفحة و/أو محتواها.
|
home.scalePages.desc=تغيير حجم/مقياس الصفحة و/أو محتواها.
|
||||||
scalePages.tags=تغيير الحجم,تعديل,الأبعاد,تكييف
|
scalePages.tags=تغيير الحجم,تعديل,الأبعاد,تكييف
|
||||||
|
|
||||||
home.pipeline.title=خط الأنابيب (متقدم)
|
home.pipeline.title=خط الأنابيب
|
||||||
home.pipeline.desc=تشغيل إجراءات متعددة على ملفات PDF عن طريق تحديد نصوص خط الأنابيب
|
home.pipeline.desc=تشغيل إجراءات متعددة على ملفات PDF عن طريق تحديد نصوص خط الأنابيب
|
||||||
pipeline.tags=أتمتة,تسلسل,مبرمج,معالجة دفعات
|
pipeline.tags=أتمتة,تسلسل,مبرمج,معالجة دفعات
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Adjust page size/scale
|
|||||||
home.scalePages.desc=Change the size/scale of page and/or its contents.
|
home.scalePages.desc=Change the size/scale of page and/or its contents.
|
||||||
scalePages.tags=resize,modify,dimension,adapt
|
scalePages.tags=resize,modify,dimension,adapt
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Advanced)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
|
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
|
||||||
pipeline.tags=automate,sequence,scripted,batch-process
|
pipeline.tags=automate,sequence,scripted,batch-process
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Upravit velikost/škálu stránky
|
|||||||
home.scalePages.desc=Změnit velikost/škálu stránky a/nebo její obsah.
|
home.scalePages.desc=Změnit velikost/škálu stránky a/nebo její obsah.
|
||||||
scalePages.tags=změnit velikost,upravit,rozměr,přizpůsobit
|
scalePages.tags=změnit velikost,upravit,rozměr,přizpůsobit
|
||||||
|
|
||||||
home.pipeline.title=Potrubí (Pokročilé)
|
home.pipeline.title=Potrubí
|
||||||
home.pipeline.desc=Spustit více akcí na PDF s definicí skriptů potrubí
|
home.pipeline.desc=Spustit více akcí na PDF s definicí skriptů potrubí
|
||||||
pipeline.tags=automatizovat,sekvence,skriptované,dávkové zpracování
|
pipeline.tags=automatizovat,sekvence,skriptované,dávkové zpracování
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Seitengröße/Skalierung anpassen
|
|||||||
home.scalePages.desc=Größe/Skalierung der Seite und/oder des Inhalts ändern
|
home.scalePages.desc=Größe/Skalierung der Seite und/oder des Inhalts ändern
|
||||||
scalePages.tags=größe ändern,ändern,dimensionieren,anpassen
|
scalePages.tags=größe ändern,ändern,dimensionieren,anpassen
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Fortgeschritten)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Mehrere Aktionen auf ein PDF anwenden, definiert durch ein Pipeline Skript
|
home.pipeline.desc=Mehrere Aktionen auf ein PDF anwenden, definiert durch ein Pipeline Skript
|
||||||
pipeline.tags=automatisieren,sequenzieren,skriptgesteuert,batch prozess
|
pipeline.tags=automatisieren,sequenzieren,skriptgesteuert,batch prozess
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ donate=Donate
|
|||||||
color=Color
|
color=Color
|
||||||
sponsor=Sponsor
|
sponsor=Sponsor
|
||||||
info=Info
|
info=Info
|
||||||
|
pro=Pro
|
||||||
page=Page
|
page=Page
|
||||||
pages=Pages
|
pages=Pages
|
||||||
|
|
||||||
@@ -110,8 +111,24 @@ pipelineOptions.pipelineHeader=Pipeline:
|
|||||||
pipelineOptions.saveButton=Download
|
pipelineOptions.saveButton=Download
|
||||||
pipelineOptions.validateButton=Validate
|
pipelineOptions.validateButton=Validate
|
||||||
|
|
||||||
|
########################
|
||||||
|
# ENTERPRISE EDITION #
|
||||||
|
########################
|
||||||
|
enterpriseEdition.button=Upgrade to Pro
|
||||||
|
enterpriseEdition.warning=This feature is only available to Pro users.
|
||||||
|
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
|
||||||
|
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
|
||||||
|
|
||||||
|
|
||||||
|
#################
|
||||||
|
# Analytics #
|
||||||
|
#################
|
||||||
|
analytics.title=Do you want make Stirling PDF better?
|
||||||
|
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
|
||||||
|
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
|
||||||
|
analytics.enable=Enable analytics
|
||||||
|
analytics.disable=Disable analytics
|
||||||
|
analytics.settings=You can change the settings for analytics in the config/settings.yml file
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# NAVBAR #
|
# NAVBAR #
|
||||||
@@ -128,6 +145,7 @@ navbar.sections.convertFrom=Convert from PDF
|
|||||||
navbar.sections.security=Sign & Security
|
navbar.sections.security=Sign & Security
|
||||||
navbar.sections.advance=Advanced
|
navbar.sections.advance=Advanced
|
||||||
navbar.sections.edit=View & Edit
|
navbar.sections.edit=View & Edit
|
||||||
|
navbar.sections.popular=Popular
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# SETTINGS #
|
# SETTINGS #
|
||||||
@@ -225,6 +243,8 @@ database.fileNotFound=File not found
|
|||||||
database.fileNullOrEmpty=File must not be null or empty
|
database.fileNullOrEmpty=File must not be null or empty
|
||||||
database.failedImportFile=Failed to import file
|
database.failedImportFile=Failed to import file
|
||||||
|
|
||||||
|
session.expired=Your session has expired. Please refresh the page and try again.
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
#############
|
#############
|
||||||
@@ -382,7 +402,7 @@ home.scalePages.title=Adjust page size/scale
|
|||||||
home.scalePages.desc=Change the size/scale of a page and/or its contents.
|
home.scalePages.desc=Change the size/scale of a page and/or its contents.
|
||||||
scalePages.tags=resize,modify,dimension,adapt
|
scalePages.tags=resize,modify,dimension,adapt
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Advanced)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
|
home.pipeline.desc=Run multiple actions on PDFs by defining pipeline scripts
|
||||||
pipeline.tags=automate,sequence,scripted,batch-process
|
pipeline.tags=automate,sequence,scripted,batch-process
|
||||||
|
|
||||||
@@ -482,10 +502,15 @@ home.removeImagePdf.title=Remove image
|
|||||||
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
home.removeImagePdf.desc=Remove image from PDF to reduce file size
|
||||||
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
|
||||||
|
|
||||||
|
|
||||||
|
home.splitPdfByChapters.title=Split PDF by Chapters
|
||||||
|
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||||
|
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=Advanced Colour options
|
||||||
replace-color.header=Replace-Invert Color PDF
|
replace-color.header=Replace-Invert Color PDF
|
||||||
home.replaceColorPdf.title=Replace and Invert Color
|
home.replaceColorPdf.title=Advanced Colour options
|
||||||
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
|
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
|
||||||
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
|
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
|
||||||
replace-color.selectText.1=Replace or Invert color Options
|
replace-color.selectText.1=Replace or Invert color Options
|
||||||
@@ -526,7 +551,10 @@ login.oauth2AccessDenied=Access Denied
|
|||||||
login.oauth2InvalidTokenResponse=Invalid Token Response
|
login.oauth2InvalidTokenResponse=Invalid Token Response
|
||||||
login.oauth2InvalidIdToken=Invalid Id Token
|
login.oauth2InvalidIdToken=Invalid Id Token
|
||||||
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
|
||||||
|
login.alreadyLoggedIn=You are already logged in to
|
||||||
|
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
|
||||||
|
login.toManySessions=You have too many active sessions
|
||||||
|
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
|
||||||
|
|
||||||
#auto-redact
|
#auto-redact
|
||||||
autoRedact.title=Auto Redact
|
autoRedact.title=Auto Redact
|
||||||
@@ -1154,7 +1182,9 @@ licenses.license=Licence
|
|||||||
survey.nav=Survey
|
survey.nav=Survey
|
||||||
survey.title=Stirling-PDF Survey
|
survey.title=Stirling-PDF Survey
|
||||||
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF!
|
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF!
|
||||||
survey.please=Please consider taking our survey!
|
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
|
||||||
|
survey.changes2=With these changes we are getting paid business support and funding
|
||||||
|
survey.please=Please consider taking our survey to have input on the future of Stirling-PDF!
|
||||||
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page)
|
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page)
|
||||||
survey.button=Take Survey
|
survey.button=Take Survey
|
||||||
survey.dontShowAgain=Don't show again
|
survey.dontShowAgain=Don't show again
|
||||||
@@ -1179,3 +1209,17 @@ removeImage.title=Remove image
|
|||||||
removeImage.header=Remove image
|
removeImage.header=Remove image
|
||||||
removeImage.removeImage=Remove image
|
removeImage.removeImage=Remove image
|
||||||
removeImage.submit=Remove image
|
removeImage.submit=Remove image
|
||||||
|
|
||||||
|
|
||||||
|
splitByChapters.title=Split PDF by Chapters
|
||||||
|
splitByChapters.header=Split PDF by Chapters
|
||||||
|
splitByChapters.bookmarkLevel=Bookmark Level
|
||||||
|
splitByChapters.includeMetadata=Include Metadata
|
||||||
|
splitByChapters.allowDuplicates=Allow Duplicates
|
||||||
|
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
|
||||||
|
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
|
||||||
|
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||||
|
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||||
|
splitByChapters.submit=Split PDF
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Escalar/ajustar tamaño de página
|
|||||||
home.scalePages.desc=Escalar/cambiar el tamaño de una pagina y/o su contenido
|
home.scalePages.desc=Escalar/cambiar el tamaño de una pagina y/o su contenido
|
||||||
scalePages.tags=cambiar tamaño,modificar,dimensionar,adaptar
|
scalePages.tags=cambiar tamaño,modificar,dimensionar,adaptar
|
||||||
|
|
||||||
home.pipeline.title=Secuencia (Avanzado)
|
home.pipeline.title=Secuencia
|
||||||
home.pipeline.desc=Ejecutar varias tareas a PDFs definiendo una secuencia de comandos
|
home.pipeline.desc=Ejecutar varias tareas a PDFs definiendo una secuencia de comandos
|
||||||
pipeline.tags=automatizar,secuencia,con script,proceso por lotes
|
pipeline.tags=automatizar,secuencia,con script,proceso por lotes
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Ajuster l’échelle ou la taille
|
|||||||
home.scalePages.desc=Modifiez la taille ou l’échelle d’une page et/ou de son contenu.
|
home.scalePages.desc=Modifiez la taille ou l’échelle d’une page et/ou de son contenu.
|
||||||
scalePages.tags=ajuster,redimensionner,resize,modify,dimension,adapt
|
scalePages.tags=ajuster,redimensionner,resize,modify,dimension,adapt
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (avancé)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Exécutez plusieurs actions sur les PDF en définissant des scripts de pipeline.
|
home.pipeline.desc=Exécutez plusieurs actions sur les PDF en définissant des scripts de pipeline.
|
||||||
pipeline.tags=automatiser,séquencer,automate,sequence,scripted,batch-process
|
pipeline.tags=automatiser,séquencer,automate,sequence,scripted,batch-process
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Prilagodite veličinu/razmjer stranice
|
|||||||
home.scalePages.desc=Promijenite veličinu/razmjer stranice i/ili njezin sadržaj.
|
home.scalePages.desc=Promijenite veličinu/razmjer stranice i/ili njezin sadržaj.
|
||||||
scalePages.tags=izmjena,modifikacija,dimenzija,adaptacija
|
scalePages.tags=izmjena,modifikacija,dimenzija,adaptacija
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Advanced)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Izvršite više radnji na PDF-ovima definiranjem skripti u pipeline-u
|
home.pipeline.desc=Izvršite više radnji na PDF-ovima definiranjem skripti u pipeline-u
|
||||||
pipeline.tags=automatizacija,sekvenciranje,skriptirano,batch-process
|
pipeline.tags=automatizacija,sekvenciranje,skriptirano,batch-process
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Menyesuaikan ukuran/skala halaman
|
|||||||
home.scalePages.desc=Mengubah ukuran/skala halaman dan/atau isinya.
|
home.scalePages.desc=Mengubah ukuran/skala halaman dan/atau isinya.
|
||||||
scalePages.tags=mengubah ukuran, memodifikasi, dimensi, mengadaptasi
|
scalePages.tags=mengubah ukuran, memodifikasi, dimensi, mengadaptasi
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Lanjutan)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Menjalankan beberapa tindakan pada PDF dengan mendefinisikan skrip pipeline
|
home.pipeline.desc=Menjalankan beberapa tindakan pada PDF dengan mendefinisikan skrip pipeline
|
||||||
pipeline.tags=mengotomatiskan, mengurutkan, menulis, proses batch
|
pipeline.tags=mengotomatiskan, mengurutkan, menulis, proses batch
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Regola le dimensioni/scala della pagina
|
|||||||
home.scalePages.desc=Modificare le dimensioni/scala della pagina e/o dei suoi contenuti.
|
home.scalePages.desc=Modificare le dimensioni/scala della pagina e/o dei suoi contenuti.
|
||||||
scalePages.tags=ridimensionare,modificare,dimensionare,adattare
|
scalePages.tags=ridimensionare,modificare,dimensionare,adattare
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (avanzato)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Esegui più azioni sui PDF definendo script di pipeline
|
home.pipeline.desc=Esegui più azioni sui PDF definendo script di pipeline
|
||||||
pipeline.tags=automatizzare,sequenziare,scriptare,elaborare in batch
|
pipeline.tags=automatizzare,sequenziare,scriptare,elaborare in batch
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=ページの縮尺の調整
|
|||||||
home.scalePages.desc=ページやコンテンツの縮尺を変更します。
|
home.scalePages.desc=ページやコンテンツの縮尺を変更します。
|
||||||
scalePages.tags=resize,modify,dimension,adapt
|
scalePages.tags=resize,modify,dimension,adapt
|
||||||
|
|
||||||
home.pipeline.title=パイプライン (高度)
|
home.pipeline.title=パイプライン
|
||||||
home.pipeline.desc=パイプラインスクリプトを定義してPDF上で複数のアクションを実行します。
|
home.pipeline.desc=パイプラインスクリプトを定義してPDF上で複数のアクションを実行します。
|
||||||
pipeline.tags=automate,sequence,scripted,batch-process
|
pipeline.tags=automate,sequence,scripted,batch-process
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Aanpassen paginaformaat/schaal
|
|||||||
home.scalePages.desc=Wijzig de grootte/schaal van een pagina en/of de inhoud ervan.
|
home.scalePages.desc=Wijzig de grootte/schaal van een pagina en/of de inhoud ervan.
|
||||||
scalePages.tags=resize,aanpassen,dimensie,aanpassen
|
scalePages.tags=resize,aanpassen,dimensie,aanpassen
|
||||||
|
|
||||||
home.pipeline.title=Pijplijn (Geavanceerd)
|
home.pipeline.title=Pijplijn
|
||||||
home.pipeline.desc=Voer meerdere acties uit op PDF's door pipelinescripts te definiëren
|
home.pipeline.desc=Voer meerdere acties uit op PDF's door pipelinescripts te definiëren
|
||||||
pipeline.tags=automatiseren,volgorde,gescrript,batch-verwerking
|
pipeline.tags=automatiseren,volgorde,gescrript,batch-verwerking
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Dopasuj rozmiar stron
|
|||||||
home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF
|
home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF
|
||||||
scalePages.tags=resize,modify,dimension,adapt
|
scalePages.tags=resize,modify,dimension,adapt
|
||||||
|
|
||||||
home.pipeline.title=Automatyzacja (Zaawansowane)
|
home.pipeline.title=Automatyzacja
|
||||||
home.pipeline.desc=Wykonaj wiele akcji na dokumentach PDF, tworząc automatyzację
|
home.pipeline.desc=Wykonaj wiele akcji na dokumentach PDF, tworząc automatyzację
|
||||||
pipeline.tags=automate,sequence,scripted,batch-process
|
pipeline.tags=automate,sequence,scripted,batch-process
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Ajustar Tamanho/Escala de Página
|
|||||||
home.scalePages.desc=Alterar o tamanho/escala da página e/ou seu conteúdo.
|
home.scalePages.desc=Alterar o tamanho/escala da página e/ou seu conteúdo.
|
||||||
scalePages.tags=redimensionar,modificar,dimensão,adaptar
|
scalePages.tags=redimensionar,modificar,dimensão,adaptar
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Avançado)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Executar várias ações em PDFs definindo scripts de pipeline
|
home.pipeline.desc=Executar várias ações em PDFs definindo scripts de pipeline
|
||||||
pipeline.tags=automatizar,sequência,scriptado,processo-em-lote
|
pipeline.tags=automatizar,sequência,scriptado,processo-em-lote
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Prispôsobiť veľkosť/škálovanie stránok
|
|||||||
home.scalePages.desc=Zmeniť veľkosť/škálovanie stránky a/alebo jej obsahu.
|
home.scalePages.desc=Zmeniť veľkosť/škálovanie stránky a/alebo jej obsahu.
|
||||||
scalePages.tags=veľkosť,modifikovať,rozmery,prispôsobiť
|
scalePages.tags=veľkosť,modifikovať,rozmery,prispôsobiť
|
||||||
|
|
||||||
home.pipeline.title=Pipeline (Pokročilé)
|
home.pipeline.title=Pipeline
|
||||||
home.pipeline.desc=Spustiť viacero akcií na PDF definovaním pipeline skriptov
|
home.pipeline.desc=Spustiť viacero akcií na PDF definovaním pipeline skriptov
|
||||||
pipeline.tags=automatizovať,sekvencia,skriptované,dávkové spracovanie
|
pipeline.tags=automatizovať,sekvencia,skriptované,dávkové spracovanie
|
||||||
|
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ home.scalePages.title=Sayfa boyutunu/ölçeğini ayarla
|
|||||||
home.scalePages.desc=Bir sayfanın ve/veya içeriğinin boyutunu/ölçeğini değiştirir
|
home.scalePages.desc=Bir sayfanın ve/veya içeriğinin boyutunu/ölçeğini değiştirir
|
||||||
scalePages.tags=boyutlandır,değiştir,boyut,uyarla
|
scalePages.tags=boyutlandır,değiştir,boyut,uyarla
|
||||||
|
|
||||||
home.pipeline.title=Çoklu İşlemler (İleri Seviye)
|
home.pipeline.title=Çoklu İşlemler
|
||||||
home.pipeline.desc=Çoklu İşlemler tanımlayarak PDF'lere birden fazla işlemi çalıştır
|
home.pipeline.desc=Çoklu İşlemler tanımlayarak PDF'lere birden fazla işlemi çalıştır
|
||||||
pipeline.tags=otomatikleştir,sıralı,betikli,toplu-işlem
|
pipeline.tags=otomatikleştir,sıralı,betikli,toplu-işlem
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ security:
|
|||||||
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
|
||||||
|
|
||||||
# Enterprise edition settings unused for now please ignore!
|
# Enterprise edition settings unused for now please ignore!
|
||||||
EnterpriseEdition:
|
enterpriseEdition:
|
||||||
|
enabled: false # set to 'true' to enable enterprise edition
|
||||||
key: 00000000-0000-0000-0000-000000000000
|
key: 00000000-0000-0000-0000-000000000000
|
||||||
CustomMetadata:
|
CustomMetadata:
|
||||||
autoUpdateMetadata: true # set to 'true' to automatically update metadata with below values
|
autoUpdateMetadata: true # set to 'true' to automatically update metadata with below values
|
||||||
@@ -72,6 +73,7 @@ system:
|
|||||||
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
|
||||||
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
|
||||||
tessdataDir: /usr/share/tessdata # Path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
tessdataDir: /usr/share/tessdata # Path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
|
||||||
|
enableAnalytics: undefined # Set to 'true' to enable analytics, set to 'false' to disable analytics, for enterprise users this is set to true
|
||||||
|
|
||||||
ui:
|
ui:
|
||||||
appName: '' # Application's visible name
|
appName: '' # Application's visible name
|
||||||
@@ -88,3 +90,4 @@ metrics:
|
|||||||
# Automatically Generated Settings (Do Not Edit Directly)
|
# Automatically Generated Settings (Do Not Edit Directly)
|
||||||
AutomaticallyGenerated:
|
AutomaticallyGenerated:
|
||||||
key: example
|
key: example
|
||||||
|
UUID: example
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -77,7 +77,7 @@ label {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
overflow-y: hidden;
|
overflow-y: auto;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
min-height: 275px;
|
min-height: 275px;
|
||||||
margin: 0 0 30px 0;
|
margin: 0 0 30px 0;
|
||||||
|
|||||||
@@ -89,6 +89,14 @@
|
|||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close-icon {
|
||||||
|
color: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-icon:hover {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
|
||||||
span.icon-text::after {
|
span.icon-text::after {
|
||||||
content: attr(data-text);
|
content: attr(data-text);
|
||||||
content: attr(data-text) / "";
|
content: attr(data-text) / "";
|
||||||
@@ -320,3 +328,26 @@ span.icon-text::after {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.go-pro-link {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-pro-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #007bff;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.go-pro-link:hover .go-pro-badge {
|
||||||
|
background-color: #0056b3;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
@@ -27,3 +27,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pdf-preview-large {
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
max-width: calc(100% - 30px);
|
||||||
|
max-height: calc(100% - 30px);
|
||||||
|
box-shadow: 0 0 4px rgba(100, 100, 100, 0.25);
|
||||||
|
transition: rotate 0.3s;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
translate: -50% -50%;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,22 @@ function showErrorBanner(message, stackTrace) {
|
|||||||
document.querySelector("#errorContainer p").textContent = message;
|
document.querySelector("#errorContainer p").textContent = message;
|
||||||
document.querySelector("#traceContent").textContent = stackTrace;
|
document.querySelector("#traceContent").textContent = stackTrace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSessionExpiredPrompt() {
|
||||||
|
const errorContainer = document.getElementById("errorContainer");
|
||||||
|
errorContainer.style.display = "block";
|
||||||
|
document.querySelector("#errorContainer .alert-heading").textContent = sessionExpired;
|
||||||
|
document.querySelector("#errorContainer p").textContent = sessionExpired;
|
||||||
|
document.querySelector("#traceContent").textContent = "";
|
||||||
|
|
||||||
|
// Optional: Add a refresh button
|
||||||
|
const refreshButton = document.createElement("button");
|
||||||
|
refreshButton.textContent = "Refresh Page";
|
||||||
|
refreshButton.className = "btn btn-primary mt-3";
|
||||||
|
refreshButton.onclick = () => location.reload();
|
||||||
|
errorContainer.appendChild(refreshButton);
|
||||||
|
}
|
||||||
|
|
||||||
let firstErrorOccurred = false;
|
let firstErrorOccurred = false;
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
@@ -79,6 +95,11 @@ async function handleSingleDownload(url, formData, isMulti = false, isZip = fals
|
|||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
// Handle 401 Unauthorized error
|
||||||
|
showSessionExpiredPrompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (contentType && contentType.includes("application/json")) {
|
if (contentType && contentType.includes("application/json")) {
|
||||||
console.error("Throwing error banner, response was not okay");
|
console.error("Throwing error banner, response was not okay");
|
||||||
return handleJsonResponse(response);
|
return handleJsonResponse(response);
|
||||||
@@ -97,7 +118,7 @@ async function handleSingleDownload(url, formData, isMulti = false, isZip = fals
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in handleSingleDownload:", error);
|
console.error("Error in handleSingleDownload:", error);
|
||||||
throw error; // Re-throw the error if you want it to be handled higher up.
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,73 @@
|
|||||||
function updateFavoritesDropdown() {
|
function updateFavoritesDropdown() {
|
||||||
var dropdown = document.querySelector("#favoritesDropdown");
|
var dropdown = document.querySelector("#favoritesDropdown");
|
||||||
|
|
||||||
// Check if dropdown exists
|
|
||||||
if (!dropdown) {
|
if (!dropdown) {
|
||||||
console.error('Dropdown element with ID "favoritesDropdown" not found!');
|
console.error('Dropdown element with ID "favoritesDropdown" not found!');
|
||||||
return; // Exit the function
|
return;
|
||||||
}
|
}
|
||||||
dropdown.innerHTML = ""; // Clear the current favorites
|
dropdown.innerHTML = "";
|
||||||
|
|
||||||
var hasFavorites = false;
|
var hasFavorites = false;
|
||||||
|
var addedFeatures = new Set();
|
||||||
|
|
||||||
for (var i = 0; i < localStorage.length; i++) {
|
for (var i = 0; i < localStorage.length; i++) {
|
||||||
var key = localStorage.key(i);
|
var key = localStorage.key(i);
|
||||||
if (localStorage.getItem(key) === "favorite") {
|
var value = localStorage.getItem(key);
|
||||||
// Find the corresponding navbar entry
|
|
||||||
|
if (value === "favorite") {
|
||||||
var navbarEntry = document.querySelector(`a[href='${key}']`);
|
var navbarEntry = document.querySelector(`a[href='${key}']`);
|
||||||
if (navbarEntry) {
|
if (navbarEntry) {
|
||||||
// Create a new dropdown entry
|
var featureName = navbarEntry.textContent.trim();
|
||||||
var dropdownItem = document.createElement("a");
|
|
||||||
dropdownItem.className = "dropdown-item";
|
if (!addedFeatures.has(featureName)) {
|
||||||
dropdownItem.href = navbarEntry.href;
|
var dropdownItem = document.createElement("div");
|
||||||
dropdownItem.innerHTML = navbarEntry.innerHTML;
|
dropdownItem.className = "dropdown-item d-flex justify-content-between align-items-center";
|
||||||
dropdown.appendChild(dropdownItem);
|
|
||||||
hasFavorites = true;
|
// Create a wrapper for the original content
|
||||||
|
var contentWrapper = document.createElement("div");
|
||||||
|
contentWrapper.className = "d-flex align-items-center flex-grow-1";
|
||||||
|
contentWrapper.style.textDecoration = "none";
|
||||||
|
contentWrapper.style.color = "inherit";
|
||||||
|
|
||||||
|
// Clone the original content
|
||||||
|
var originalContent = navbarEntry.querySelector('div').cloneNode(true);
|
||||||
|
contentWrapper.appendChild(originalContent);
|
||||||
|
|
||||||
|
// Create the remove button
|
||||||
|
var removeButton = document.createElement("button");
|
||||||
|
removeButton.className = "btn btn-sm btn-link p-0 ml-2";
|
||||||
|
removeButton.innerHTML = '<i class="material-symbols-rounded close-icon" style="font-size: 18px;">close</i>';
|
||||||
|
removeButton.onclick = function(itemKey, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
localStorage.removeItem(itemKey);
|
||||||
|
updateFavoritesSection();
|
||||||
|
updateFavoritesDropdown();
|
||||||
|
filterCards();
|
||||||
|
}.bind(null, key);
|
||||||
|
|
||||||
|
// Add click event to the content wrapper
|
||||||
|
contentWrapper.onclick = function(itemHref, event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.location.href = itemHref;
|
||||||
|
}.bind(null, navbarEntry.href);
|
||||||
|
|
||||||
|
dropdownItem.appendChild(contentWrapper);
|
||||||
|
dropdownItem.appendChild(removeButton);
|
||||||
|
dropdown.appendChild(dropdownItem);
|
||||||
|
hasFavorites = true;
|
||||||
|
addedFeatures.add(featureName);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Navbar entry not found for key: ${key}`);
|
console.warn(`Navbar entry not found for key: ${key}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show or hide the default item based on whether there are any favorites
|
|
||||||
if (!hasFavorites) {
|
if (!hasFavorites) {
|
||||||
var defaultItem = document.createElement("a");
|
var defaultItem = document.createElement("a");
|
||||||
defaultItem.className = "dropdown-item";
|
defaultItem.className = "dropdown-item";
|
||||||
defaultItem.textContent = noFavourites;
|
defaultItem.textContent = noFavourites || "No favorites added";
|
||||||
dropdown.appendChild(defaultItem);
|
dropdown.appendChild(defaultItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the DOM content has been fully loaded before calling the function
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
|
||||||
console.log("DOMContentLoaded event fired");
|
|
||||||
updateFavoritesDropdown();
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -25,29 +25,38 @@ function filterCards() {
|
|||||||
function updateFavoritesSection() {
|
function updateFavoritesSection() {
|
||||||
const favoritesContainer = document.getElementById("groupFavorites").querySelector(".feature-group-container");
|
const favoritesContainer = document.getElementById("groupFavorites").querySelector(".feature-group-container");
|
||||||
favoritesContainer.style.maxHeight = "none";
|
favoritesContainer.style.maxHeight = "none";
|
||||||
favoritesContainer.innerHTML = "";
|
favoritesContainer.innerHTML = ""; // Clear the container first
|
||||||
const cards = Array.from(document.querySelectorAll(".feature-card"));
|
const cards = Array.from(document.querySelectorAll(".feature-card:not(.duplicate)"));
|
||||||
|
const addedCardIds = new Set(); // To keep track of added card IDs
|
||||||
let favoritesAmount = 0;
|
let favoritesAmount = 0;
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
if (localStorage.getItem(card.id) === "favorite") {
|
if (localStorage.getItem(card.id) === "favorite" && !addedCardIds.has(card.id)) {
|
||||||
const duplicate = card.cloneNode(true);
|
const duplicate = card.cloneNode(true);
|
||||||
|
duplicate.classList.add("duplicate");
|
||||||
favoritesContainer.appendChild(duplicate);
|
favoritesContainer.appendChild(duplicate);
|
||||||
|
addedCardIds.add(card.id); // Mark this card as added
|
||||||
favoritesAmount++;
|
favoritesAmount++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (favoritesAmount === 0) {
|
if (favoritesAmount === 0) {
|
||||||
document.getElementById("groupFavorites").style.display = "none";
|
document.getElementById("groupFavorites").style.display = "none";
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("groupFavorites").style.display = "flex";
|
document.getElementById("groupFavorites").style.display = "flex";
|
||||||
};
|
}
|
||||||
reorderCards(favoritesContainer);
|
reorderCards(favoritesContainer);
|
||||||
favoritesContainer.style.maxHeight = favoritesContainer.scrollHeight + "px";
|
favoritesContainer.style.maxHeight = favoritesContainer.scrollHeight + "px";
|
||||||
};
|
}
|
||||||
|
|
||||||
function toggleFavorite(element) {
|
function toggleFavorite(element) {
|
||||||
var span = element.querySelector("span.material-symbols-rounded");
|
var span = element.querySelector("span.material-symbols-rounded");
|
||||||
var card = element.closest(".feature-card");
|
var card = element.closest(".feature-card");
|
||||||
var cardId = card.id;
|
var cardId = card.id;
|
||||||
|
|
||||||
|
// Prevent the event from bubbling up to parent elements
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
if (span.classList.contains("no-fill")) {
|
if (span.classList.contains("no-fill")) {
|
||||||
span.classList.remove("no-fill");
|
span.classList.remove("no-fill");
|
||||||
span.classList.add("fill");
|
span.classList.add("fill");
|
||||||
@@ -59,7 +68,31 @@ function toggleFavorite(element) {
|
|||||||
card.classList.remove("favorite");
|
card.classList.remove("favorite");
|
||||||
localStorage.removeItem(cardId);
|
localStorage.removeItem(cardId);
|
||||||
}
|
}
|
||||||
reorderCards(card.parentNode);
|
|
||||||
|
// Use setTimeout to ensure this runs after the current call stack is clear
|
||||||
|
setTimeout(() => {
|
||||||
|
reorderCards(card.parentNode);
|
||||||
|
updateFavoritesSection();
|
||||||
|
updateFavoritesDropdown();
|
||||||
|
filterCards();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFavorites() {
|
||||||
|
const cards = Array.from(document.querySelectorAll(".feature-card"));
|
||||||
|
cards.forEach(card => {
|
||||||
|
const isFavorite = localStorage.getItem(card.id) === "favorite";
|
||||||
|
const starIcon = card.querySelector(".favorite-icon span.material-symbols-rounded");
|
||||||
|
if (isFavorite) {
|
||||||
|
starIcon.classList.remove("no-fill");
|
||||||
|
starIcon.classList.add("fill");
|
||||||
|
card.classList.add("favorite");
|
||||||
|
} else {
|
||||||
|
starIcon.classList.remove("fill");
|
||||||
|
starIcon.classList.add("no-fill");
|
||||||
|
card.classList.remove("favorite");
|
||||||
|
}
|
||||||
|
});
|
||||||
updateFavoritesSection();
|
updateFavoritesSection();
|
||||||
updateFavoritesDropdown();
|
updateFavoritesDropdown();
|
||||||
filterCards();
|
filterCards();
|
||||||
@@ -181,7 +214,10 @@ function expandCollapseAll(expandAll) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = initializeCards;
|
window.onload = function() {
|
||||||
|
initializeCards();
|
||||||
|
syncFavorites(); // Ensure everything is in sync on page load
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
|
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
|
||||||
@@ -223,7 +259,5 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
})
|
})
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
showFavoritesOnly();
|
showFavoritesOnly();
|
||||||
});
|
});
|
||||||
@@ -192,6 +192,7 @@ class PdfActionsManager {
|
|||||||
|
|
||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PdfActionsManager;
|
export default PdfActionsManager;
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ class PdfContainer {
|
|||||||
const newAngle = lastAngle + deg;
|
const newAngle = lastAngle + deg;
|
||||||
|
|
||||||
element.style.rotate = newAngle + "deg";
|
element.style.rotate = newAngle + "deg";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPdfFile(file, nextSiblingElement) {
|
async addPdfFile(file, nextSiblingElement) {
|
||||||
@@ -326,6 +327,9 @@ class PdfContainer {
|
|||||||
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
|
page.setRotation(PDFLib.degrees(page.getRotation().angle + rotationAngle));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pdfDoc.setCreator(stirlingPDFLabel);
|
||||||
|
pdfDoc.setProducer(stirlingPDFLabel);
|
||||||
|
|
||||||
const pdfBytes = await pdfDoc.save();
|
const pdfBytes = await pdfDoc.save();
|
||||||
const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" });
|
const pdfBlob = new Blob([pdfBytes], { type: "application/pdf" });
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
const scrollDivHorizontally = (id) => {
|
const scrollDivHorizontally = (id) => {
|
||||||
var scrollDelta = 0; // variable to store the accumulated scroll delta
|
var scrollDeltaX = 0; // variable to store the accumulated horizontal scroll delta
|
||||||
|
var scrollDeltaY = 0; // variable to store the accumulated vertical scroll delta
|
||||||
var isScrolling = false; // variable to track if scroll is already in progress
|
var isScrolling = false; // variable to track if scroll is already in progress
|
||||||
const divToScrollHorizontally = document.getElementById(id);
|
const divToScroll = document.getElementById(id);
|
||||||
|
|
||||||
function scrollLoop() {
|
function scrollLoop() {
|
||||||
// Scroll the div horizontally by a fraction of the accumulated scroll delta
|
// Scroll the div horizontally and vertically by a fraction of the accumulated scroll delta
|
||||||
divToScrollHorizontally.scrollLeft += scrollDelta * 0.1;
|
divToScroll.scrollLeft += scrollDeltaX * 0.1;
|
||||||
|
divToScroll.scrollTop += scrollDeltaY * 0.1;
|
||||||
|
|
||||||
// Reduce the accumulated scroll delta by a fraction
|
// Reduce the accumulated scroll delta by a fraction
|
||||||
scrollDelta *= 0.9;
|
scrollDeltaX *= 0.9;
|
||||||
|
scrollDeltaY *= 0.9;
|
||||||
|
|
||||||
// If scroll delta is still significant, continue the scroll loop
|
// If scroll delta is still significant, continue the scroll loop
|
||||||
if (Math.abs(scrollDelta) > 0.1) {
|
if (Math.abs(scrollDeltaX) > 0.1 || Math.abs(scrollDeltaY) > 0.1) {
|
||||||
requestAnimationFrame(scrollLoop);
|
requestAnimationFrame(scrollLoop);
|
||||||
} else {
|
} else {
|
||||||
isScrolling = false; // Reset scroll in progress flag
|
isScrolling = false; // Reset scroll in progress flag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
divToScrollHorizontally.addEventListener("wheel", function (e) {
|
divToScroll.addEventListener("wheel", function (e) {
|
||||||
e.preventDefault(); // prevent default mousewheel behavior
|
e.preventDefault(); // prevent default mousewheel behavior
|
||||||
|
|
||||||
// Accumulate the horizontal scroll delta
|
// Accumulate the horizontal and vertical scroll delta
|
||||||
scrollDelta -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
|
scrollDeltaX -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
|
||||||
|
scrollDeltaY -= e.deltaY || e.wheelDeltaY || -e.deltaX || -e.wheelDeltaX;
|
||||||
|
|
||||||
// If scroll is not already in progress, start the scroll loop
|
// If scroll is not already in progress, start the scroll loop
|
||||||
if (!isScrolling) {
|
if (!isScrolling) {
|
||||||
@@ -31,4 +36,4 @@ const scrollDivHorizontally = (id) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default scrollDivHorizontally;
|
export default scrollDivHorizontally;
|
||||||
@@ -278,7 +278,7 @@
|
|||||||
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
|
let allKeys = new Set([...Object.keys(accountSettings), ...Object.keys(localStorage)]);
|
||||||
|
|
||||||
allKeys.forEach(key => {
|
allKeys.forEach(key => {
|
||||||
if(key === 'debug' || key === '0' || key === '1') return; // Ignoring specific keys
|
if(key === 'debug' || key === '0' || key === '1' || key.includes('pdfjs') || key.includes('posthog') || key.includes('pageViews')) return; // Ignoring specific keys
|
||||||
|
|
||||||
const accountValue = accountSettings[key] || '-';
|
const accountValue = accountSettings[key] || '-';
|
||||||
const browserValue = localStorage.getItem(key) || '-';
|
const browserValue = localStorage.getItem(key) || '-';
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
|
|
||||||
// Then, set the account settings to local storage
|
// Then, set the account settings to local storage
|
||||||
for (let key in accountSettings) {
|
for (let key in accountSettings) {
|
||||||
if(key !== 'debug' && key !== '0' && key !== '1') { // Only sync non-ignored keys
|
if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) { // Only sync non-ignored keys
|
||||||
localStorage.setItem(key, accountSettings[key]);
|
localStorage.setItem(key, accountSettings[key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if(key !== 'debug' && key !== '0' && key !== '1') { // Only send non-ignored keys
|
if(key !== 'debug' && key !== '0' && key !== '1' && !key.includes('pdfjs') && !key.includes('posthog') && !key.includes('pageViews')) { // Only send non-ignored keys
|
||||||
let hiddenField = document.createElement("input");
|
let hiddenField = document.createElement("input");
|
||||||
hiddenField.type = "hidden";
|
hiddenField.type = "hidden";
|
||||||
hiddenField.name = key;
|
hiddenField.name = key;
|
||||||
|
|||||||
@@ -101,6 +101,8 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<p th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||||
|
|
||||||
<script th:inline="javascript">
|
<script th:inline="javascript">
|
||||||
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
||||||
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="favorite-icon" onclick="toggleFavorite(this)">
|
<div class="favorite-icon" onclick="toggleFavorite(this)">
|
||||||
<span class="material-symbols-rounded no-fill">
|
<span class="material-symbols-rounded">
|
||||||
star
|
star
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,6 +69,49 @@
|
|||||||
<script th:src="@{'/js/cacheFormInputs.js'}" th:if="${currentPage != 'home'}"></script>
|
<script th:src="@{'/js/cacheFormInputs.js'}" th:if="${currentPage != 'home'}"></script>
|
||||||
<script th:src="@{'/js/tab-container.js'}"></script>
|
<script th:src="@{'/js/tab-container.js'}"></script>
|
||||||
<script th:src="@{'/js/darkmode.js'}"></script>
|
<script th:src="@{'/js/darkmode.js'}"></script>
|
||||||
|
<script th:inline="javascript">
|
||||||
|
const stirlingPDFLabel = /*[[${@StirlingPDFLabel}]]*/ '';
|
||||||
|
const analyticsEnabled = /*[[${@analyticsEnabled}]]*/ false;
|
||||||
|
|
||||||
|
if (analyticsEnabled) {
|
||||||
|
!function (t, e) {
|
||||||
|
var o, n, p, r;
|
||||||
|
e.__SV || (window.posthog = e, e._i = [], e.init = function (i, s, a) {
|
||||||
|
function g(t, e) {
|
||||||
|
var o = e.split(".");
|
||||||
|
2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () {
|
||||||
|
t.push([e].concat(Array.prototype.slice.call(arguments, 0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(p = t.createElement("script")).type = "text/javascript", p.async = !0, p.src = s.api_host + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r);
|
||||||
|
var u = e;
|
||||||
|
for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) {
|
||||||
|
var e = "posthog";
|
||||||
|
return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e
|
||||||
|
}, u.people.toString = function () {
|
||||||
|
return u.toString(1) + ".people (stub)"
|
||||||
|
}, o = "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId".split(" "), n = 0; n < o.length; n++) g(u, o[n]);
|
||||||
|
e._i.push([i, s, a])
|
||||||
|
}, e.__SV = 1)
|
||||||
|
}(document, window.posthog || []);
|
||||||
|
posthog.init('phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq', {
|
||||||
|
api_host: 'https://eu.i.posthog.com',
|
||||||
|
persistence: 'localStorage',
|
||||||
|
person_profiles: 'always',
|
||||||
|
mask_all_text: true,
|
||||||
|
mask_all_element_attributes: true
|
||||||
|
})
|
||||||
|
const baseUrl = window.location.hostname;
|
||||||
|
posthog.register_once({
|
||||||
|
'hostname': baseUrl,
|
||||||
|
'UUID': /*[[${@UUID}]]*/ ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</th:block>
|
</th:block>
|
||||||
|
|
||||||
<th:block th:fragment="game">
|
<th:block th:fragment="game">
|
||||||
@@ -148,6 +191,7 @@
|
|||||||
const multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false;
|
const multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false;
|
||||||
const disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false;
|
const disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false;
|
||||||
const remoteCall = /*[[${remoteCall}]]*/ true;
|
const remoteCall = /*[[${remoteCall}]]*/ true;
|
||||||
|
const sessionExpired = /*[[#{session.expired}]]*/ '';
|
||||||
</script>
|
</script>
|
||||||
<script th:src="@{'/js/downloader.js'}"></script>
|
<script th:src="@{'/js/downloader.js'}"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -357,6 +357,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank" rel="noopener noreferrer">
|
||||||
|
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<!-- Settings Button -->
|
<!-- Settings Button -->
|
||||||
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal">
|
<a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal">
|
||||||
|
|||||||
@@ -68,11 +68,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="popularTools" class="feature-group">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div id="groupOrganize" class="feature-group">
|
<div id="groupOrganize" class="feature-group">
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
|
th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-group-container">
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -199,6 +227,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', toolIcon='water_drop', tags=#{watermark.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -215,9 +246,7 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='text_fields', tags=#{addImage.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='text_fields', tags=#{addImage.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', toolIcon='water_drop', tags=#{watermark.tags}, toolGroup='security')}">
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', toolIcon='assignment', tags=#{changeMetadata.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', toolIcon='assignment', tags=#{changeMetadata.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -227,9 +256,6 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='photo_library', tags=#{extractImages.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='photo_library', tags=#{extractImages.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -263,15 +289,15 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='advance')}">
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', toolIcon='palette', tags=#{adjust-contrast.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', toolIcon='palette', tags=#{adjust-contrast.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
||||||
</div>
|
</div>
|
||||||
@@ -303,8 +329,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<!-- Survey Modal -->
|
<!-- Survey Modal -->
|
||||||
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel" aria-hidden="true">
|
||||||
aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -312,13 +337,12 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p th:text="#{survey.description}">Stirling-PDF has no tracking so we want to hear from our users to improve
|
<p th:text="#{survey.changes}">Stirling-PDF has changed since the last survey! To find out more please check our blog post here:</h5>
|
||||||
Stirling-PDF!</h5>
|
<a href="https://stirlingpdf.info/blog/stirling-pdf-survey-results" target="_blank" th:text="#{survey.changes2}">https://stirlingpdf.info/blog/stirling-pdf-survey-results</a>
|
||||||
|
<p th:text="#{survey.changes2}">With these changes we are getting paid business support and funding</p>
|
||||||
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||||
<p th:text="#{survey.disabled}">Survey popup will be disabled in following updates but available at foot of
|
<p th:text="#{survey.disabled}">Survey popup will be disabled in following updates but available at foot of page)</p>
|
||||||
page)</p>
|
<a href="https://stirlingpdf.info/s/clwzgtfw7000gltkmwz1n212m" target="_blank" class="btn btn-primary" id="takeSurvey"th:text="#{survey.button}" >Take Survey</a>
|
||||||
<a href="https://stirlingpdf.info/s/clwzgtfw7000gltkmwz1n212m" target="_blank" class="btn btn-primary"
|
|
||||||
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-3">
|
||||||
@@ -333,18 +357,99 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
/*
|
<!-- Analytics Modal -->
|
||||||
|
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel" aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF better?</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.</p>
|
||||||
|
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
|
||||||
|
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
|
||||||
|
/*<![CDATA[*/
|
||||||
|
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
if (analyticsPromptBoolean) {
|
||||||
|
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
||||||
|
analyticsModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*]]>*/
|
||||||
|
function setAnalytics(enabled) {
|
||||||
|
fetch('api/v1/settings/update-enable-analytics', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(enabled)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('Analytics setting updated successfully');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
||||||
|
} else if (response.status === 208) {
|
||||||
|
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
||||||
|
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response status: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error updating analytics setting:', error);
|
||||||
|
alert('An error occurred while updating the analytics setting. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
const surveyVersion = "1.1";
|
const surveyVersion = "2.0";
|
||||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||||
|
|
||||||
if (localStorage.getItem('surveyVersion') !== surveyVersion || !localStorage.getItem('dontShowSurvey')) {
|
const viewThresholds = [5, 15, 30, 50, 75, 100, 150, 200];
|
||||||
modal.show();
|
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||||
|
|
||||||
|
pageViews++;
|
||||||
|
localStorage.setItem('pageViews', pageViews.toString());
|
||||||
|
|
||||||
|
function shouldShowSurvey() {
|
||||||
|
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localStorage.getItem('surveyVersion') !== surveyVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewThresholds.includes(pageViews);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldShowSurvey()) {
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
dontShowAgain.addEventListener('change', function() {
|
dontShowAgain.addEventListener('change', function() {
|
||||||
if (this.checked) {
|
if (this.checked) {
|
||||||
@@ -357,15 +462,11 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
takeSurveyButton.addEventListener('click', function() {
|
takeSurveyButton.addEventListener('click', function() {
|
||||||
localStorage.setItem('dontShowSurvey', 'true');
|
localStorage.setItem('surveyTaken', 'true');
|
||||||
localStorage.setItem('surveyVersion', surveyVersion);
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
modal.hide();
|
modal.hide();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
if (localStorage.getItem('dontShowSurvey')) {
|
|
||||||
modal.hide();
|
|
||||||
}
|
|
||||||
});*/
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
64
src/main/resources/templates/split-pdf-by-chapters.html
Normal file
64
src/main/resources/templates/split-pdf-by-chapters.html
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 bg-card">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="material-symbols-rounded tool-header-icon organize">book</span>
|
||||||
|
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
|
||||||
|
</div>
|
||||||
|
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
|
||||||
|
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
|
||||||
|
<label class="form-check-label" for="includeMetadata" th:text="#{splitByChapters.includeMetadata}"></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
|
||||||
|
<label class="form-check-label" for="allowDuplicates" th:text="#{splitByChapters.allowDuplicates}"></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||||
|
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||||
|
</p>
|
||||||
|
<div class="collapse" id="info">
|
||||||
|
<p th:text="#{splitByChapters.desc.1}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.2}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.3}"></p>
|
||||||
|
<p th:text="#{splitByChapters.desc.4}"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user