From 5832147b3082dd45edd393b8e69b36dea2819c9e Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com.> Date: Tue, 1 Oct 2024 13:43:08 +0100 Subject: [PATCH] Merge branch 'main' of git@github.com:Stirling-Tools/Stirling-PDF.git into main --- build.gradle | 1 + .../software/SPDF/EE/EEAppConfig.java | 8 +- .../SPDF/EE/KeygenLicenseVerifier.java | 170 ++++---- .../software/SPDF/EE/LicenseKeyChecker.java | 41 +- .../software/SPDF/LibreOfficeListener.java | 4 +- .../software/SPDF/config/AppConfig.java | 11 +- .../SPDF/config/AppUpdateService.java | 1 + .../software/SPDF/config/InitialSetup.java | 42 ++ .../{Beans.java => LocaleConfiguration.java} | 2 +- .../software/SPDF/config/PostHogConfig.java | 34 ++ .../FingerprintBasedSessionFilter.java | 66 ++++ .../FingerprintBasedSessionManager.java | 130 ++++++ .../fingerprint/FingerprintGenerator.java | 77 ++++ .../DatabaseBackupInterface.java | 2 +- .../{ => interfaces}/ShowAdminInterface.java | 2 +- .../config/security/AppUpdateAuthService.java | 2 +- .../config/security/InitialSecuritySetup.java | 45 +-- .../security/SecurityConfiguration.java | 7 +- .../SPDF/config/security/UserService.java | 2 +- .../database/DatabaseBackupHelper.java | 2 +- .../SPDF/controller/api/CropController.java | 7 +- .../controller/api/SettingsController.java | 37 ++ .../controller/api/SplitPDFController.java | 2 - .../api/SplitPdfByChaptersController.java | 2 +- .../SPDF/controller/api/UserController.java | 7 +- .../api/misc/ExtractImagesController.java | 2 - .../api/misc/PrintFileController.java | 5 +- .../api/security/RedactController.java | 2 - .../SPDF/model/ApplicationProperties.java | 6 +- .../software/SPDF/pdf/TextFinder.java | 4 +- .../SPDF/service/CustomPDDocumentFactory.java | 1 - .../service/MetricsAggregatorService.java | 55 +++ .../PdfMetadataService.java | 2 +- .../software/SPDF/service/PostHogService.java | 374 ++++++++++++++++++ .../software/SPDF/utils/GeneralUtils.java | 39 ++ .../software/SPDF/utils/PDFToFile.java | 1 - src/main/resources/application.properties | 2 + src/main/resources/messages_en_GB.properties | 24 +- src/main/resources/settings.yml.template | 3 + .../resources/templates/fragments/common.html | 41 ++ src/main/resources/templates/home.html | 66 +++- 41 files changed, 1129 insertions(+), 202 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/InitialSetup.java rename src/main/java/stirling/software/SPDF/config/{Beans.java => LocaleConfiguration.java} (97%) create mode 100644 src/main/java/stirling/software/SPDF/config/PostHogConfig.java create mode 100644 src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionFilter.java create mode 100644 src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionManager.java create mode 100644 src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintGenerator.java rename src/main/java/stirling/software/SPDF/config/{ => interfaces}/DatabaseBackupInterface.java (85%) rename src/main/java/stirling/software/SPDF/config/{ => interfaces}/ShowAdminInterface.java (69%) create mode 100644 src/main/java/stirling/software/SPDF/controller/api/SettingsController.java create mode 100644 src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java rename src/main/java/stirling/software/SPDF/{config => service}/PdfMetadataService.java (98%) create mode 100644 src/main/java/stirling/software/SPDF/service/PostHogService.java diff --git a/build.gradle b/build.gradle index ce09352a..13f187ad 100644 --- a/build.gradle +++ b/build.gradle @@ -127,6 +127,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-jetty:$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") { implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index 11a5041f..4769d446 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -17,9 +17,11 @@ public class EEAppConfig { @Autowired ApplicationProperties applicationProperties; - @Bean(name = "RunningEE") + @Autowired + private LicenseKeyChecker licenseKeyChecker; + + @Bean(name = "runningEE") public boolean runningEnterpriseEdition() { - return applicationProperties.getEnterpriseEdition().getEnabled(); - // TODO: check EE license key + return licenseKeyChecker.getEnterpriseEnabledResult(); } } diff --git a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java index 564388aa..737dbd1c 100644 --- a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java +++ b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java @@ -14,58 +14,58 @@ 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; @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 String PUBLIC_KEY = - "-----BEGIN PUBLIC KEY-----\n" - + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzJaf7jPx/bamT/ctmvrf\n" - + "5HfzV9CrTx39Hv48NvRIjw9jBAlmcSndLbgcrTUWFrd7pJPPEhzmfJ9tLRg0a3Si\n" - + "34Ed9gQ24mODj0Wpos5uwwxu1M5wzsKPjkLZDigB3d9L/79nyKvSUo+mx+dZmZnD\n" - + "D19TMM93ZDxG+Bru5/rvvxaZzMHZAnqrTdoO55vFjpss5XJNt6kz4jxr+D6a3lFU\n" - + "GGCx7bjeanHCNGRw84dLYbU8s5DGsx5JNX1xPGR1kODocvsHfHJvsxfdNtpH4vke\n" - + "yOrtEUCp01Mh2kr3zM8R4Yjh4ae2qHiZne0FiVhiUaHmbf2dmcA9O1Kynz33634s\n" - + "fwIDAQAB\n" - + "-----END PUBLIC KEY-----"; private static final ObjectMapper objectMapper = new ObjectMapper(); - public static boolean verifyLicense(String licenseKey) { + public boolean verifyLicense(String licenseKey) { try { String machineFingerprint = generateMachineFingerprint(); // First, try to validate the license - boolean isValid = validateLicense(licenseKey, machineFingerprint); - - // If validation fails, try to activate the machine - if (!isValid) { - System.out.println( - "License validation failed. Attempting to activate the machine..."); - isValid = activateMachine(licenseKey, machineFingerprint); - - if (isValid) { - // If activation is successful, try to validate again - isValid = validateLicense(licenseKey, machineFingerprint); + JsonNode validationResponse = validateLicense(licenseKey, machineFingerprint); + log.debug(validationResponse.asText()); + 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 isValid; + return false; } catch (Exception e) { - System.out.println("Error verifying license: " + e.getMessage()); + log.error("Error verifying license: " + e.getMessage()); return false; } } - private static boolean validateLicense(String licenseKey, String machineFingerprint) + private static JsonNode validateLicense(String licenseKey, String machineFingerprint) throws Exception { HttpClient client = HttpClient.newHttpClient(); String requestBody = String.format( - "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\",\"product\":\"%s\"}}}", - licenseKey, machineFingerprint, PRODUCT_ID); - + "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}", + licenseKey, machineFingerprint ); HttpRequest request = HttpRequest.newBuilder() .uri( @@ -76,107 +76,81 @@ public class KeygenLicenseVerifier { + "/licenses/actions/validate-key")) .header("Content-Type", "application/vnd.api+json") .header("Accept", "application/vnd.api+json") - .header("Authorization", "license " + licenseKey) + //.header("Authorization", "License " + licenseKey) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - + log.debug(" validateLicenseResponse body: " + response.body()); + JsonNode jsonResponse = objectMapper.readTree(response.body()); if (response.statusCode() == 200) { - JsonNode jsonResponse = objectMapper.readTree(response.body()); + JsonNode metaNode = jsonResponse.path("meta"); boolean isValid = metaNode.path("valid").asBoolean(); + String detail = metaNode.path("detail").asText(); String code = metaNode.path("code").asText(); - System.out.println("License validity: " + isValid); - System.out.println("Validation detail: " + detail); - System.out.println("Validation code: " + code); + log.debug("License validity: " + isValid); + log.debug("Validation detail: " + detail); + log.debug("Validation code: " + code); - if (isValid) { - return verifySignature(metaNode); - } + } else { - System.out.println("Error validating license. Status code: " + response.statusCode()); - System.out.println("Response body: " + response.body()); + log.error("Error validating license. Status code: " + response.statusCode()); } - return false; + return jsonResponse; } - private static boolean activateMachine(String licenseKey, String machineFingerprint) + private static boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint) throws Exception { HttpClient client = HttpClient.newHttpClient(); - String requestBody = - String.format( - "{\"data\":{\"type\":\"machines\",\"attributes\":{\"fingerprint\":\"%s\"},\"relationships\":{\"license\":{\"data\":{\"type\":\"licenses\",\"id\":\"%s\"}}}}}", - machineFingerprint, licenseKey); - String licenseId = "8e072b67-3cea-454b-98bb-bb73bbc04bd4"; - 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 " + licenseId) - .POST(HttpRequest.BodyPublishers.ofString(requestBody)) - .build(); + 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); - + log.debug("activateMachine Response body: " + response.body()); if (response.statusCode() == 201) { - System.out.println("Machine activated successfully"); + log.info("Machine activated successfully"); return true; } else { - System.out.println("Error activating machine. Status code: " + response.statusCode()); - System.out.println("Response body: " + response.body()); + log.error("Error activating machine. Status code: " + response.statusCode()); + return false; } } - private static boolean verifySignature(JsonNode metaNode) throws Exception { - String signature = metaNode.path("signature").asText(); - String data = metaNode.path("data").asText(); - - PublicKey publicKey = - KeyFactory.getInstance("RSA") - .generatePublic( - new X509EncodedKeySpec( - Base64.getDecoder() - .decode( - PUBLIC_KEY - .replace( - "-----BEGIN PUBLIC KEY-----", - "") - .replace( - "-----END PUBLIC KEY-----", - "") - .replaceAll("\\s", "")))); - - Signature sig = Signature.getInstance("SHA256withRSA"); - sig.initVerify(publicKey); - sig.update(data.getBytes()); - - boolean isSignatureValid = sig.verify(Base64.getDecoder().decode(signature)); - System.out.println("Signature validity: " + isSignatureValid); - return isSignatureValid; - } private static String generateMachineFingerprint() { // This is a simplified example. In a real-world scenario, you'd want to generate // a more robust and unique fingerprint based on hardware characteristics. - return "example-fingerprint-" + System.currentTimeMillis(); + return "example-fingerprint"; } - public static void test() { - String[] testKeys = { - "FYKJ-YK7F-MEVX-RYKK-JYWE-77WW-3TKN-PJRU", "EFDB57-92B4C2-EDFA20-51146E-E1AF4A-V3" - }; - - for (String licenseKey : testKeys) { - System.out.println("Testing license key: " + licenseKey); - boolean isValid = verifyLicense(licenseKey); - System.out.println("License is valid: " + isValid); - System.out.println("--------------------"); - } - } } diff --git a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java index 7ca54e8c..3ce11c95 100644 --- a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java +++ b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java @@ -1,18 +1,22 @@ package stirling.software.SPDF.EE; +import java.io.IOException; + import org.springframework.boot.CommandLineRunner; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.utils.GeneralUtils; @Component -public class LicenseKeyChecker implements CommandLineRunner { +public class LicenseKeyChecker { private final KeygenLicenseVerifier licenseService; private final ApplicationProperties applicationProperties; + private static boolean enterpriseEnbaledResult = false; // Inject your license service or configuration public LicenseKeyChecker( KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { @@ -20,39 +24,28 @@ public class LicenseKeyChecker implements CommandLineRunner { this.applicationProperties = new ApplicationProperties(); } - // Validate on startup - @Override - public void run(String... args) throws Exception { - checkLicense(); - } - // Periodic license check - runs every 7 days @Scheduled(fixedRate = 604800000) // 7 days in milliseconds public void checkLicensePeriodically() { checkLicense(); } - // License validation logic private void checkLicense() { - boolean isValid = - licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey()); - if (!isValid) { - // Handle invalid license (shut down the app, log, etc.) - System.out.println("License key is invalid!"); - // Optionally stop the application - // System.exit(1); // Uncomment if you want to stop the app - } else { - System.out.println("License key is valid."); - } + if(!applicationProperties.getEnterpriseEdition().isEnabled()) { + enterpriseEnbaledResult = false; + } else { + enterpriseEnbaledResult = licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey()); + } + } - // Method to update the license key dynamically - public void updateLicenseKey(String newKey) { - // Update the key in ApplicationProperties + public void updateLicenseKey(String newKey) throws IOException { applicationProperties.getEnterpriseEdition().setKey(newKey); - - // Immediately validate the new key - System.out.println("License key has been updated. Checking new key..."); + GeneralUtils.saveKeyToConfig("EnterpriseEdition.key", newKey, false); checkLicense(); } + + public boolean getEnterpriseEnabledResult() { + return enterpriseEnbaledResult; + } } diff --git a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java index 06e19512..5e66b8a8 100644 --- a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java +++ b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java @@ -10,7 +10,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.github.pixee.security.SystemCommand; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class LibreOfficeListener { private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class); @@ -31,7 +33,7 @@ public class LibreOfficeListener { private LibreOfficeListener() {} private boolean isListenerRunning() { - System.out.println("waiting for listener to start"); + log.info("waiting for listener to start"); try (Socket socket = new Socket()) { socket.connect( new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 7635cafe..88ed34ea 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -20,6 +20,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.thymeleaf.spring6.SpringTemplateEngine; +import stirling.software.SPDF.EE.LicenseKeyChecker; import stirling.software.SPDF.model.ApplicationProperties; @Configuration @@ -30,6 +31,7 @@ public class AppConfig { @Autowired ApplicationProperties applicationProperties; + @Bean @ConditionalOnProperty( name = "system.customHTMLFiles", @@ -169,7 +171,7 @@ public class AppConfig { @Bean(name = "analyticsEnabled") public boolean analyticsEnabled() { - if (applicationProperties.getEnterpriseEdition().getEnabled()) return true; + if (applicationProperties.getEnterpriseEdition().isEnabled()) return true; return applicationProperties.getSystem().getEnableAnalytics() != null && Boolean.parseBoolean(applicationProperties.getSystem().getEnableAnalytics()); } @@ -178,4 +180,11 @@ public class AppConfig { public String stirlingPDFLabel() { return "Stirling-PDF" + " v" + appVersion(); } + + @Bean(name = "UUID") + public String uuid() { + return applicationProperties.getAutomaticallyGenerated().getUUID(); + } + + } diff --git a/src/main/java/stirling/software/SPDF/config/AppUpdateService.java b/src/main/java/stirling/software/SPDF/config/AppUpdateService.java index 7fc87629..3eb20488 100644 --- a/src/main/java/stirling/software/SPDF/config/AppUpdateService.java +++ b/src/main/java/stirling/software/SPDF/config/AppUpdateService.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; +import stirling.software.SPDF.config.interfaces.ShowAdminInterface; import stirling.software.SPDF.model.ApplicationProperties; @Service diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java new file mode 100644 index 00000000..043b08c9 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -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); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/Beans.java b/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java similarity index 97% rename from src/main/java/stirling/software/SPDF/config/Beans.java rename to src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java index 03084b24..a15d6c16 100644 --- a/src/main/java/stirling/software/SPDF/config/Beans.java +++ b/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java @@ -14,7 +14,7 @@ import org.springframework.web.servlet.i18n.SessionLocaleResolver; import stirling.software.SPDF.model.ApplicationProperties; @Configuration -public class Beans implements WebMvcConfigurer { +public class LocaleConfiguration implements WebMvcConfigurer { @Autowired ApplicationProperties applicationProperties; diff --git a/src/main/java/stirling/software/SPDF/config/PostHogConfig.java b/src/main/java/stirling/software/SPDF/config/PostHogConfig.java new file mode 100644 index 00000000..42d5b101 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/PostHogConfig.java @@ -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(); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionFilter.java b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionFilter.java new file mode 100644 index 00000000..6106e977 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionFilter.java @@ -0,0 +1,66 @@ +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); + } +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionManager.java b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionManager.java new file mode 100644 index 00000000..9f85defc --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintBasedSessionManager.java @@ -0,0 +1,130 @@ +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.beans.factory.annotation.Autowired; +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 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> iterator = activeFingerprints.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry 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; + + + } +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintGenerator.java b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintGenerator.java new file mode 100644 index 00000000..fc24cd3e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/fingerprint/FingerprintGenerator.java @@ -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); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java similarity index 85% rename from src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java rename to src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java index 267981d1..3ad11f2a 100644 --- a/src/main/java/stirling/software/SPDF/config/DatabaseBackupInterface.java +++ b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseBackupInterface.java @@ -1,4 +1,4 @@ -package stirling.software.SPDF.config; +package stirling.software.SPDF.config.interfaces; import java.io.IOException; import java.util.List; diff --git a/src/main/java/stirling/software/SPDF/config/ShowAdminInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/ShowAdminInterface.java similarity index 69% rename from src/main/java/stirling/software/SPDF/config/ShowAdminInterface.java rename to src/main/java/stirling/software/SPDF/config/interfaces/ShowAdminInterface.java index e49376e2..1bbebf5a 100644 --- a/src/main/java/stirling/software/SPDF/config/ShowAdminInterface.java +++ b/src/main/java/stirling/software/SPDF/config/interfaces/ShowAdminInterface.java @@ -1,4 +1,4 @@ -package stirling.software.SPDF.config; +package stirling.software.SPDF.config.interfaces; public interface ShowAdminInterface { default boolean getShowUpdateOnlyAdmins() { diff --git a/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java b/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java index 5a16aa30..57ce7b7d 100644 --- a/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java +++ b/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java @@ -7,7 +7,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; 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.User; import stirling.software.SPDF.repository.UserRepository; diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index b7442c5b..f43baf0a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -1,19 +1,14 @@ package stirling.software.SPDF.config.security; import java.io.IOException; -import java.nio.file.Path; -import java.nio.file.Paths; 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.stereotype.Component; import jakarta.annotation.PostConstruct; 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.Role; @@ -39,15 +34,6 @@ public class InitialSecuritySetup { 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 { String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); @@ -89,33 +75,4 @@ public class InitialSecuritySetup { 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; - } - } } diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index aa266d2f..352b6184 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -75,10 +75,9 @@ public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - if (loginEnabledValue) { - + http.addFilterBefore( + userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); http.csrf(csrf -> csrf.disable()); http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); @@ -86,7 +85,7 @@ public class SecurityConfiguration { sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - .maximumSessions(10) + .maximumSessions(4) .maxSessionsPreventsLogin(false) .sessionRegistry(sessionRegistry) .expiredUrl("/login?logout=true")); diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index ece81355..39b26a0e 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -19,7 +19,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.core.user.OAuth2User; 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.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.model.AuthenticationType; diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java index 3fd7993f..6ccd0ac3 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseBackupHelper.java @@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import lombok.extern.slf4j.Slf4j; -import stirling.software.SPDF.config.DatabaseBackupInterface; +import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.utils.FileInfo; @Slf4j diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index 551cd72d..d386d1ce 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.service.CustomPDDocumentFactory; +import stirling.software.SPDF.service.PostHogService; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @@ -36,9 +37,13 @@ public class CropController { private final CustomPDDocumentFactory pdfDocumentFactory; + private final PostHogService postHogService; + @Autowired - public CropController(CustomPDDocumentFactory pdfDocumentFactory) { + public CropController( + CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) { this.pdfDocumentFactory = pdfDocumentFactory; + this.postHogService = postHogService; } @PostMapping(value = "/crop", consumes = "multipart/form-data") diff --git a/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java new file mode 100644 index 00000000..dd674210 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -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 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"); + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 2da76fa3..e27df103 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -60,8 +60,6 @@ public class SplitPDFController { // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); int totalPages = document.getNumberOfPages(); List pageNumbers = request.getPageNumbersList(document, false); - System.out.println( - pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); if (!pageNumbers.contains(totalPages - 1)) { // Create a mutable ArrayList so we can add to it pageNumbers = new ArrayList<>(pageNumbers); diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index 9d03f8b6..9690dde7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -32,9 +32,9 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; -import stirling.software.SPDF.config.PdfMetadataService; import stirling.software.SPDF.model.PdfMetadata; import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; +import stirling.software.SPDF.service.PdfMetadataService; import stirling.software.SPDF.utils.WebResponseUtils; @RestController diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 1f33832e..e9dfb066 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -30,6 +30,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.model.AuthenticationType; @@ -40,6 +41,7 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass; @Controller @Tag(name = "User", description = "User APIs") @RequestMapping("/api/v1/user") +@Slf4j public class UserController { @Autowired private UserService userService; @@ -191,13 +193,12 @@ public class UserController { Map paramMap = request.getParameterMap(); Map updates = new HashMap<>(); - System.out.println("Received parameter map: " + paramMap); - + for (Map.Entry entry : paramMap.entrySet()) { 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 userService.updateUserSettings(principal.getName(), updates); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 3dff43e8..075de0d1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -60,8 +60,6 @@ public class ExtractImagesController { MultipartFile file = request.getFileInput(); String format = request.getFormat(); boolean allowDuplicates = request.isAllowDuplicates(); - System.out.println( - System.currentTimeMillis() + " file=" + file.getName() + ", format=" + format); PDDocument document = Loader.loadPDF(file.getBytes()); // Determine if multithreading should be used based on PDF size or number of pages diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java index bc0a6715..4ca068d5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java @@ -25,12 +25,13 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.tags.Tag; - +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.PrintFileRequest; @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") +@Slf4j public class PrintFileController { // TODO @@ -59,7 +60,7 @@ public class PrintFileController { new IllegalArgumentException( "No matching printer found")); - System.out.println("Selected Printer: " + selectedService.getName()); + log.info("Selected Printer: " + selectedService.getName()); if ("application/pdf".equals(contentType)) { PDDocument document = Loader.loadPDF(file.getBytes()); diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 40dc2c75..d9e1e4ca 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -58,7 +58,6 @@ public class RedactController { float customPadding = request.getCustomPadding(); boolean convertPDFToImage = request.isConvertPDFToImage(); - System.out.println(listOfTextString); String[] listOfText = listOfTextString.split("\n"); PDDocument document = pdfDocumentFactory.load(file); @@ -75,7 +74,6 @@ public class RedactController { for (String text : listOfText) { text = text.trim(); - System.out.println(text); TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); List foundTexts = textFinder.getTextLocations(document); redactFoundText(document, foundTexts, customPadding, redactColor); diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index fa4ec575..60fd1287 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import lombok.Data; import lombok.ToString; @@ -24,6 +26,7 @@ import stirling.software.SPDF.model.provider.UnsupportedProviderException; @ConfigurationProperties(prefix = "") @PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class) @Data +@Order(Ordered.HIGHEST_PRECEDENCE) public class ApplicationProperties { private Legal legal = new Legal(); @@ -176,11 +179,12 @@ public class ApplicationProperties { @Data public static class AutomaticallyGenerated { @ToString.Exclude private String key; + private String UUID; } @Data public static class EnterpriseEdition { - private Boolean enabled; + private boolean enabled; @ToString.Exclude private String key; private CustomMetadata customMetadata = new CustomMetadata(); diff --git a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index f9e339c2..77f7d0c6 100644 --- a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -10,8 +10,10 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.TextPosition; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.PDFText; +@Slf4j public class TextFinder extends PDFTextStripper { private final String searchText; @@ -92,7 +94,7 @@ public class TextFinder extends PDFTextStripper { public List getTextLocations(PDDocument document) throws Exception { this.getText(document); - System.out.println( + log.debug( "Found " + textOccurrences.size() + " occurrences of '" diff --git a/src/main/java/stirling/software/SPDF/service/CustomPDDocumentFactory.java b/src/main/java/stirling/software/SPDF/service/CustomPDDocumentFactory.java index b3f9eec7..6e62aba7 100644 --- a/src/main/java/stirling/software/SPDF/service/CustomPDDocumentFactory.java +++ b/src/main/java/stirling/software/SPDF/service/CustomPDDocumentFactory.java @@ -11,7 +11,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; -import stirling.software.SPDF.config.PdfMetadataService; import stirling.software.SPDF.model.PdfMetadata; import stirling.software.SPDF.model.api.PDFFile; diff --git a/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java b/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java new file mode 100644 index 00000000..10026459 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java @@ -0,0 +1,55 @@ +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 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 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); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/PdfMetadataService.java b/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java similarity index 98% rename from src/main/java/stirling/software/SPDF/config/PdfMetadataService.java rename to src/main/java/stirling/software/SPDF/service/PdfMetadataService.java index c5092189..373b4c91 100644 --- a/src/main/java/stirling/software/SPDF/config/PdfMetadataService.java +++ b/src/main/java/stirling/software/SPDF/service/PdfMetadataService.java @@ -1,4 +1,4 @@ -package stirling.software.SPDF.config; +package stirling.software.SPDF.service; import java.util.Calendar; diff --git a/src/main/java/stirling/software/SPDF/service/PostHogService.java b/src/main/java/stirling/software/SPDF/service/PostHogService.java new file mode 100644 index 00000000..b7e8dd41 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/service/PostHogService.java @@ -0,0 +1,374 @@ +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() { + try { + postHog.capture(uniqueId, "system_info_captured", captureServerMetrics()); + + } catch (Exception e) { + // Handle exceptions + } + } + + public void captureEvent(String eventName, Map properties) { + postHog.capture(uniqueId, eventName, properties); + } + + public Map captureServerMetrics() { + Map 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 getDockerMetrics() { + Map 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 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 captureApplicationProperties() { + Map 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 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 getNetworkInterfacesInfo() { + Map interfacesInfo = new HashMap<>(); + try { + Enumeration 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; + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 21d921c8..be07780b 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -16,7 +16,12 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; 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.LoggerFactory; import org.springframework.web.multipart.MultipartFile; @@ -262,4 +267,38 @@ public class GeneralUtils { } 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(); + } } diff --git a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java index 920e987b..1a1957cf 100644 --- a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java +++ b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java @@ -191,7 +191,6 @@ public class PDFToFile { Files.deleteIfExists(tempInputFile); if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } - System.out.println("fileBytes=" + fileBytes.length); return WebResponseUtils.bytesToWebResponse( fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 58b0ec47..c57449dd 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -49,3 +49,5 @@ springdoc.api-docs.path=/v1/api-docs springdoc.swagger-ui.url=/v1/api-docs +posthog.api.key=phc_Bh95TRT3qZveAxJpBmJcPpSpW2dJeiKlgin8c7xXGna +posthog.host=https://eu.i.posthog.com \ No newline at end of file diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 4c2a40cb..68b5f2cd 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -76,6 +76,7 @@ donate=Donate color=Color sponsor=Sponsor info=Info +pro=Pro legal.privacy=Privacy Policy legal.terms=Terms and Conditions @@ -108,8 +109,24 @@ pipelineOptions.pipelineHeader=Pipeline: pipelineOptions.saveButton=Download 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 # @@ -383,7 +400,7 @@ home.scalePages.title=Adjust page size/scale home.scalePages.desc=Change the size/scale of a page and/or its contents. 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 pipeline.tags=automate,sequence,scripted,batch-process @@ -510,7 +527,10 @@ login.oauth2AccessDenied=Access Denied login.oauth2InvalidTokenResponse=Invalid Token Response login.oauth2InvalidIdToken=Invalid Id Token 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 autoRedact.title=Auto Redact diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 2a5400b6..216221f6 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -50,6 +50,7 @@ security: # Enterprise edition settings unused for now please ignore! EnterpriseEdition: + enabled: false # set to 'true' to enable enterprise edition key: 00000000-0000-0000-0000-000000000000 CustomMetadata: 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' customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files tessdataDir: /usr/share/tessdata # Path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. + enableAnalytics: undefined # Set to 'true' to enable analytics, set to 'false' to disable analytics, for enterprise users this is set to true ui: appName: '' # Application's visible name @@ -88,3 +90,4 @@ metrics: # Automatically Generated Settings (Do Not Edit Directly) AutomaticallyGenerated: key: example + UUID: example diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index 0f317b86..4ad17a20 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -71,7 +71,48 @@ + + + diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 255867d4..68ec23da 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -353,7 +353,71 @@ -