From 9884c65b10acfccfdd2974f3700fa4343df9f54a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:52:53 +0000 Subject: [PATCH] formattingand autowired constructors (#2557) # Description This pull request includes several changes aimed at improving the code structure and removing redundant code. The most significant changes involve reordering methods, removing unnecessary annotations, and refactoring constructors to use dependency injection. Autowired now comes via constructor (which also doesn't need autowired annotation as its done by default for configuration) ## Checklist - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have performed a self-review of my own code - [ ] I have attached images of the change if it is UI based - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [ ] My changes generate no new warnings - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) --- .../signature/CreateSignatureBase.java | 16 +- .../pdfbox/examples/signature/TSAClient.java | 6 +- .../software/SPDF/EE/EEAppConfig.java | 12 +- .../software/SPDF/LibreOfficeListener.java | 12 +- .../software/SPDF/SPdfApplication.java | 64 ++-- .../software/SPDF/config/AppConfig.java | 9 +- .../SPDF/config/AppUpdateService.java | 12 +- .../SPDF/config/EndpointConfiguration.java | 7 +- .../SPDF/config/EndpointInterceptor.java | 7 +- .../SPDF/config/ExternalAppDepConfig.java | 37 +-- .../software/SPDF/config/InitialSetup.java | 20 +- .../SPDF/config/LocaleConfiguration.java | 16 +- .../software/SPDF/config/OpenApiConfig.java | 11 +- .../software/SPDF/config/WebMvcConfig.java | 7 +- .../config/security/AppUpdateAuthService.java | 18 +- .../security/CustomUserDetailsService.java | 14 +- .../config/security/FirstLoginFilter.java | 12 +- .../config/security/IPRateLimitingFilter.java | 6 +- .../config/security/InitialSecuritySetup.java | 16 +- .../config/security/LoginAttemptService.java | 17 +- .../security/SecurityConfiguration.java | 151 ++++----- .../security/UserBasedRateLimitingFilter.java | 23 +- .../SPDF/config/security/UserService.java | 65 ++-- .../database/DatabaseBackupHelper.java | 7 +- .../security/database/ScheduledTasks.java | 7 +- .../session/SessionPersistentRegistry.java | 7 +- .../security/session/SessionScheduled.java | 9 +- .../api/AdditionalLanguageJsController.java | 33 +- .../controller/api/DatabaseController.java | 19 +- .../api/MultiPageLayoutController.java | 2 +- .../api/PdfImageRemovalController.java | 5 +- .../controller/api/SettingsController.java | 12 +- .../api/SplitPdfByChaptersController.java | 136 ++++---- .../SPDF/controller/api/UserController.java | 78 +---- .../converters/ConvertOfficeController.java | 14 +- .../ConvertPDFToBookController.java | 29 +- .../api/misc/AutoRenameController.java | 20 +- .../api/misc/AutoSplitPdfController.java | 99 +++--- .../api/misc/BlankPageController.java | 52 +-- .../api/misc/CompressController.java | 2 +- .../api/misc/ExtractImageScansController.java | 4 +- .../api/misc/FakeScanControllerWIP.java | 13 +- .../api/misc/MetadataController.java | 6 +- .../controller/api/misc/OCRController.java | 45 +-- .../api/misc/PrintFileController.java | 3 +- .../controller/api/misc/StampController.java | 2 +- .../api/pipeline/ApiDocService.java | 29 +- .../api/pipeline/PipelineController.java | 30 +- .../pipeline/PipelineDirectoryProcessor.java | 37 ++- .../api/pipeline/PipelineProcessor.java | 121 +++---- .../api/security/CertSignController.java | 307 +++++++++--------- .../controller/api/security/GetInfoOnPDF.java | 100 +++--- .../api/security/RedactController.java | 2 +- .../api/security/SanitizeController.java | 12 +- .../security/ValidateSignatureController.java | 6 +- .../api/security/WatermarkController.java | 2 +- .../controller/web/AccountWebController.java | 86 ++--- .../controller/web/DatabaseWebController.java | 11 +- .../controller/web/GeneralWebController.java | 93 +++--- .../controller/web/HomeWebController.java | 9 +- .../controller/web/MetricsController.java | 58 ++-- .../controller/web/OtherWebController.java | 7 +- .../controller/web/SignatureController.java | 17 +- .../SPDF/model/ApplicationProperties.java | 34 +- .../software/SPDF/model/Authority.java | 25 +- .../stirling/software/SPDF/model/Role.java | 32 +- .../stirling/software/SPDF/model/User.java | 8 +- .../SPDF/model/provider/GithubProvider.java | 9 +- .../SPDF/model/provider/GoogleProvider.java | 9 +- .../software/SPDF/pdf/TextFinder.java | 20 +- .../repository/JPATokenRepositoryImpl.java | 7 +- .../service/CertificateValidationService.java | 21 +- .../software/SPDF/service/PostHogService.java | 6 +- .../software/SPDF/utils/FileInfo.java | 5 +- .../software/SPDF/utils/FileToPdf.java | 7 +- .../software/SPDF/utils/GeneralUtils.java | 13 +- .../SPDF/utils/ImageProcessingUtils.java | 6 +- .../software/SPDF/utils/PdfUtils.java | 152 ++++----- .../software/SPDF/utils/ProcessExecutor.java | 50 ++- .../misc/CustomColorReplaceStrategy.java | 5 +- src/main/resources/application.properties | 17 +- src/main/resources/logback.xml | 13 +- .../software/SPDF/SPdfApplicationTest.java | 4 +- .../api/RearrangePagesPDFControllerTest.java | 13 +- .../converters/ConvertWebsiteToPdfTest.java | 6 +- .../software/SPDF/utils/FileToPdfTest.java | 7 +- .../software/SPDF/utils/GeneralUtilsTest.java | 185 +++++------ .../SPDF/utils/ImageProcessingUtilsTest.java | 14 +- .../software/SPDF/utils/PdfUtilsTest.java | 8 +- .../SPDF/utils/ProcessExecutorTest.java | 6 +- .../SPDF/utils/RequestUriUtilsTest.java | 4 +- .../SPDF/utils/WebResponseUtilsTest.java | 16 +- 92 files changed, 1256 insertions(+), 1535 deletions(-) diff --git a/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java b/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java index 646561f0d..aba11d9b0 100644 --- a/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java +++ b/src/main/java/org/apache/pdfbox/examples/signature/CreateSignatureBase.java @@ -97,14 +97,14 @@ public abstract class CreateSignatureBase implements SignatureInterface { this.privateKey = privateKey; } - public final void setCertificateChain(final Certificate[] certificateChain) { - this.certificateChain = certificateChain; - } - public Certificate[] getCertificateChain() { return certificateChain; } + public final void setCertificateChain(final Certificate[] certificateChain) { + this.certificateChain = certificateChain; + } + public void setTsaUrl(String tsaUrl) { this.tsaUrl = tsaUrl; } @@ -152,6 +152,10 @@ public abstract class CreateSignatureBase implements SignatureInterface { } } + public boolean isExternalSigning() { + return externalSigning; + } + /** * Set if external signing scenario should be used. If {@code false}, SignatureInterface would * be used for signing. @@ -163,8 +167,4 @@ public abstract class CreateSignatureBase implements SignatureInterface { public void setExternalSigning(boolean externalSigning) { this.externalSigning = externalSigning; } - - public boolean isExternalSigning() { - return externalSigning; - } } diff --git a/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java b/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java index a215fe525..6f57e205a 100644 --- a/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java +++ b/src/main/java/org/apache/pdfbox/examples/signature/TSAClient.java @@ -51,15 +51,13 @@ public class TSAClient { private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER = new DefaultDigestAlgorithmIdentifierFinder(); - + // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux + private static final Random RANDOM = new SecureRandom(); private final URL url; private final String username; private final String password; private final MessageDigest digest; - // SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux - private static final Random RANDOM = new SecureRandom(); - /** * @param url the URL of the TSA service * @param username user name of TSA diff --git a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java index b5fb3556f..1d73cd93e 100644 --- a/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java +++ b/src/main/java/stirling/software/SPDF/EE/EEAppConfig.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.EE; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -14,8 +13,15 @@ import stirling.software.SPDF.model.ApplicationProperties; @Slf4j public class EEAppConfig { - @Autowired ApplicationProperties applicationProperties; - @Autowired private LicenseKeyChecker licenseKeyChecker; + private final ApplicationProperties applicationProperties; + + private final LicenseKeyChecker licenseKeyChecker; + + public EEAppConfig( + ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) { + this.applicationProperties = applicationProperties; + this.licenseKeyChecker = licenseKeyChecker; + } @Bean(name = "runningEE") public boolean runningEnterpriseEdition() { diff --git a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java index 7c78287d2..5b00700e8 100644 --- a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java +++ b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java @@ -17,18 +17,16 @@ public class LibreOfficeListener { private static final LibreOfficeListener INSTANCE = new LibreOfficeListener(); private static final int LISTENER_PORT = 2002; + private ExecutorService executorService; + private long lastActivityTime; + private Process process; + + private LibreOfficeListener() {} public static LibreOfficeListener getInstance() { return INSTANCE; } - private ExecutorService executorService; - private long lastActivityTime; - - private Process process; - - private LibreOfficeListener() {} - private boolean isListenerRunning() { log.info("waiting for listener to start"); try (Socket socket = new Socket()) { diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index e26fb1117..10eecaaa9 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -1,6 +1,5 @@ package stirling.software.SPDF; -import java.awt.*; import java.io.IOException; import java.net.ServerSocket; import java.nio.file.Files; @@ -11,8 +10,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Properties; -import javax.swing.*; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; @@ -34,24 +31,22 @@ import stirling.software.SPDF.model.ApplicationProperties; @Slf4j public class SPdfApplication { - @Autowired private Environment env; - @Autowired ApplicationProperties applicationProperties; - private static String baseUrlStatic; private static String serverPortStatic; + private final Environment env; + private final ApplicationProperties applicationProperties; + private final WebBrowser webBrowser; @Value("${baseUrl:http://localhost}") private String baseUrl; - @Value("${server.port:8080}") - public void setServerPortStatic(String port) { - if ("auto".equalsIgnoreCase(port)) { - // Use Spring Boot's automatic port assignment (server.port=0) - SPdfApplication.serverPortStatic = - "0"; // This will let Spring Boot assign an available port - } else { - SPdfApplication.serverPortStatic = port; - } + public SPdfApplication( + Environment env, + ApplicationProperties applicationProperties, + @Autowired(required = false) WebBrowser webBrowser) { + this.env = env; + this.applicationProperties = applicationProperties; + this.webBrowser = webBrowser; } // Optionally keep this method if you want to provide a manual port-incrementation fallback. @@ -72,29 +67,23 @@ public class SPdfApplication { } public static void main(String[] args) throws IOException, InterruptedException { - SpringApplication app = new SpringApplication(SPdfApplication.class); - Properties props = new Properties(); - if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { System.setProperty("java.awt.headless", "false"); app.setHeadless(false); props.put("java.awt.headless", "false"); props.put("spring.main.web-application-type", "servlet"); } - app.setAdditionalProfiles("default"); app.addInitializers(new ConfigInitializer()); Map propertyFiles = new HashMap<>(); - // External config files if (Files.exists(Paths.get("configs/settings.yml"))) { propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml"); } else { log.warn("External configuration file 'configs/settings.yml' does not exist."); } - if (Files.exists(Paths.get("configs/custom_settings.yml"))) { String existingLocation = propertyFiles.getOrDefault("spring.config.additional-location", ""); @@ -108,21 +97,17 @@ public class SPdfApplication { log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); } Properties finalProps = new Properties(); - if (!propertyFiles.isEmpty()) { finalProps.putAll( Collections.singletonMap( "spring.config.additional-location", propertyFiles.get("spring.config.additional-location"))); } - if (!props.isEmpty()) { finalProps.putAll(props); } app.setDefaultProperties(finalProps); - app.run(args); - // Ensure directories are created try { Files.createDirectories(Path.of("customFiles/static/")); @@ -130,7 +115,6 @@ public class SPdfApplication { } catch (Exception e) { log.error("Error creating directories: {}", e.getMessage()); } - printStartupLogs(); } @@ -140,8 +124,24 @@ public class SPdfApplication { log.info("Navigate to {}", url); } - @Autowired(required = false) - private WebBrowser webBrowser; + public static String getStaticBaseUrl() { + return baseUrlStatic; + } + + public static String getStaticPort() { + return serverPortStatic; + } + + @Value("${server.port:8080}") + public void setServerPortStatic(String port) { + if ("auto".equalsIgnoreCase(port)) { + // Use Spring Boot's automatic port assignment (server.port=0) + SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port + "0"; + } else { + SPdfApplication.serverPortStatic = port; + } + } @PostConstruct public void init() { @@ -180,18 +180,10 @@ public class SPdfApplication { } } - public static String getStaticBaseUrl() { - return baseUrlStatic; - } - public String getNonStaticBaseUrl() { return baseUrlStatic; } - public static String getStaticPort() { - return serverPortStatic; - } - public String getNonStaticPort() { return serverPortStatic; } diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 6d8786e85..19a4b768f 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -7,7 +7,6 @@ import java.nio.file.Paths; import java.util.Properties; import java.util.function.Predicate; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -27,7 +26,11 @@ import stirling.software.SPDF.model.ApplicationProperties; @Slf4j public class AppConfig { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public AppConfig(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @Bean @ConditionalOnProperty( @@ -106,13 +109,11 @@ public class AppConfig { if (!Files.exists(dockerEnv)) { return true; } - Path mountInfo = Paths.get("/proc/1/mountinfo"); // this should always exist, if not some unknown usecase if (!Files.exists(mountInfo)) { return true; } - try { return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs ")); } catch (IOException e) { diff --git a/src/main/java/stirling/software/SPDF/config/AppUpdateService.java b/src/main/java/stirling/software/SPDF/config/AppUpdateService.java index 3eb204887..faeab2c39 100644 --- a/src/main/java/stirling/software/SPDF/config/AppUpdateService.java +++ b/src/main/java/stirling/software/SPDF/config/AppUpdateService.java @@ -11,10 +11,16 @@ import stirling.software.SPDF.model.ApplicationProperties; @Service class AppUpdateService { - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; - @Autowired(required = false) - ShowAdminInterface showAdmin; + private final ShowAdminInterface showAdmin; + + public AppUpdateService( + ApplicationProperties applicationProperties, + @Autowired(required = false) ShowAdminInterface showAdmin) { + this.applicationProperties = applicationProperties; + this.showAdmin = showAdmin; + } @Bean(name = "shouldShow") @Scope("request") diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 998d7525e..fdbc7015f 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -20,11 +20,10 @@ import stirling.software.SPDF.model.ApplicationProperties; @DependsOn({"bookAndHtmlFormatsInstalled"}) public class EndpointConfiguration { + private static final String REMOVE_BLANKS = "remove-blanks"; + private final ApplicationProperties applicationProperties; private Map endpointStatuses = new ConcurrentHashMap<>(); private Map> endpointGroups = new ConcurrentHashMap<>(); - - private final ApplicationProperties applicationProperties; - private boolean bookAndHtmlFormatsInstalled; @Autowired @@ -287,6 +286,4 @@ public class EndpointConfiguration { public Set getEndpointsForGroup(String group) { return endpointGroups.getOrDefault(group, new HashSet<>()); } - - private static final String REMOVE_BLANKS = "remove-blanks"; } diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 81b50b840..8c60ca90d 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -10,7 +9,11 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class EndpointInterceptor implements HandlerInterceptor { - @Autowired private EndpointConfiguration endpointConfiguration; + private final EndpointConfiguration endpointConfiguration; + + public EndpointInterceptor(EndpointConfiguration endpointConfiguration) { + this.endpointConfiguration = endpointConfiguration; + } @Override public boolean preHandle( diff --git a/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java b/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java index 968e60c13..9d1ac1fcc 100644 --- a/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java +++ b/src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import jakarta.annotation.PostConstruct; @@ -15,7 +14,24 @@ import lombok.extern.slf4j.Slf4j; @Configuration @Slf4j public class ExternalAppDepConfig { - @Autowired private EndpointConfiguration endpointConfiguration; + + private final EndpointConfiguration endpointConfiguration; + private final Map> commandToGroupMapping = + new HashMap<>() { + + { + put("soffice", List.of("LibreOffice")); + put("weasyprint", List.of("Weasyprint")); + put("pdftohtml", List.of("Pdftohtml")); + put("unoconv", List.of("Unoconv")); + put("qpdf", List.of("qpdf")); + put("tesseract", List.of("tesseract")); + } + }; + + public ExternalAppDepConfig(EndpointConfiguration endpointConfiguration) { + this.endpointConfiguration = endpointConfiguration; + } private boolean isCommandAvailable(String command) { try { @@ -34,18 +50,6 @@ public class ExternalAppDepConfig { } } - private final Map> commandToGroupMapping = - new HashMap<>() { - { - put("soffice", List.of("LibreOffice")); - put("weasyprint", List.of("Weasyprint")); - put("pdftohtml", List.of("Pdftohtml")); - put("unoconv", List.of("Unoconv")); - put("qpdf", List.of("qpdf")); - put("tesseract", List.of("tesseract")); - } - }; - private List getAffectedFeatures(String group) { return endpointConfiguration.getEndpointsForGroup(group).stream() .map(endpoint -> formatEndpointAsFeature(endpoint)) @@ -55,7 +59,6 @@ public class ExternalAppDepConfig { private String formatEndpointAsFeature(String endpoint) { // First replace common terms String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); - // Split into words and capitalize each word return Arrays.stream(feature.split("\\s+")) .map(word -> capitalizeWord(word)) @@ -76,7 +79,6 @@ public class ExternalAppDepConfig { boolean isAvailable = isCommandAvailable(command); if (!isAvailable) { List affectedGroups = commandToGroupMapping.get(command); - if (affectedGroups != null) { for (String group : affectedGroups) { List affectedFeatures = getAffectedFeatures(group); @@ -95,7 +97,6 @@ public class ExternalAppDepConfig { @PostConstruct public void checkDependencies() { - // Check core dependencies checkDependencyAndDisableGroup("tesseract"); checkDependencyAndDisableGroup("soffice"); @@ -103,13 +104,11 @@ public class ExternalAppDepConfig { checkDependencyAndDisableGroup("weasyprint"); checkDependencyAndDisableGroup("pdftohtml"); checkDependencyAndDisableGroup("unoconv"); - // Special handling for Python/OpenCV dependencies boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python"); if (!pythonAvailable) { List pythonFeatures = getAffectedFeatures("Python"); List openCVFeatures = getAffectedFeatures("OpenCV"); - endpointConfiguration.disableGroup("Python"); endpointConfiguration.disableGroup("OpenCV"); log.warn( diff --git a/src/main/java/stirling/software/SPDF/config/InitialSetup.java b/src/main/java/stirling/software/SPDF/config/InitialSetup.java index 81279ce9f..c8053b6fa 100644 --- a/src/main/java/stirling/software/SPDF/config/InitialSetup.java +++ b/src/main/java/stirling/software/SPDF/config/InitialSetup.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.util.Properties; 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.core.io.ClassPathResource; @@ -23,25 +22,26 @@ import stirling.software.SPDF.utils.GeneralUtils; @Order(Ordered.HIGHEST_PRECEDENCE + 1) public class InitialSetup { - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public InitialSetup(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @PostConstruct public void init() throws IOException { initUUIDKey(); - initSecretKey(); - initEnableCSRFSecurity(); - initLegalUrls(); - initSetAppVersion(); } 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 + // Generating a random UUID as the secret key + uuid = UUID.randomUUID().toString(); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid); applicationProperties.getAutomaticallyGenerated().setUUID(uuid); } @@ -50,7 +50,8 @@ public class InitialSetup { 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 + // Generating a random UUID as the secret key + secretKey = UUID.randomUUID().toString(); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey); applicationProperties.getAutomaticallyGenerated().setKey(secretKey); } @@ -76,7 +77,6 @@ public class InitialSetup { GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); } - // Initialize Privacy Policy String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); if (StringUtils.isEmpty(privacyUrl)) { @@ -87,7 +87,6 @@ public class InitialSetup { } public void initSetAppVersion() throws IOException { - String appVersion = "0.0.0"; Resource resource = new ClassPathResource("version.properties"); Properties props = new Properties(); @@ -95,7 +94,6 @@ public class InitialSetup { props.load(resource.getInputStream()); appVersion = props.getProperty("version"); } catch (Exception e) { - } applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false); diff --git a/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java b/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java index a15d6c165..94c3392f9 100644 --- a/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/LocaleConfiguration.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.config; import java.util.Locale; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.LocaleResolver; @@ -16,7 +15,11 @@ import stirling.software.SPDF.model.ApplicationProperties; @Configuration public class LocaleConfiguration implements WebMvcConfigurer { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public LocaleConfiguration(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @Override public void addInterceptors(InterceptorRegistry registry) { @@ -34,21 +37,17 @@ public class LocaleConfiguration implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver slr = new SessionLocaleResolver(); - String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); - Locale defaultLocale = - Locale.UK; // Fallback to UK locale if environment variable is not set - + Locale defaultLocale = // Fallback to UK locale if environment variable is not set + Locale.UK; if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); String tempLanguageTag = tempLocale.toLanguageTag(); - if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale; } else { tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-")); tempLanguageTag = tempLocale.toLanguageTag(); - if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale; } else { @@ -57,7 +56,6 @@ public class LocaleConfiguration implements WebMvcConfigurer { } } } - slr.setDefaultLocale(defaultLocale); return slr; } diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 7194f4a2c..0734b2317 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,15 +14,19 @@ import stirling.software.SPDF.model.ApplicationProperties; @Configuration public class OpenApiConfig { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public OpenApiConfig(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @Bean public OpenAPI customOpenAPI() { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { - version = "1.0.0"; // default version if all else fails + // default version if all else fails + version = "1.0.0"; } - SecurityScheme apiKeyScheme = new SecurityScheme() .type(SecurityScheme.Type.APIKEY) diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index eaadd251f..5cbae1f0f 100644 --- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; @@ -9,7 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { - @Autowired private EndpointInterceptor endpointInterceptor; + private final EndpointInterceptor endpointInterceptor; + + public WebMvcConfig(EndpointInterceptor endpointInterceptor) { + this.endpointInterceptor = endpointInterceptor; + } @Override public void addInterceptors(InterceptorRegistry registry) { 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 57ce7b7d6..c8a133223 100644 --- a/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java +++ b/src/main/java/stirling/software/SPDF/config/security/AppUpdateAuthService.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.config.security; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; @@ -15,8 +14,15 @@ import stirling.software.SPDF.repository.UserRepository; @Service class AppUpdateAuthService implements ShowAdminInterface { - @Autowired private UserRepository userRepository; - @Autowired private ApplicationProperties applicationProperties; + private final UserRepository userRepository; + + private final ApplicationProperties applicationProperties; + + public AppUpdateAuthService( + UserRepository userRepository, ApplicationProperties applicationProperties) { + this.userRepository = userRepository; + this.applicationProperties = applicationProperties; + } @Override public boolean getShowUpdateOnlyAdmins() { @@ -24,24 +30,18 @@ class AppUpdateAuthService implements ShowAdminInterface { if (!showUpdate) { return showUpdate; } - boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin(); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { return !showUpdateOnlyAdmin; } - if (authentication.getName().equalsIgnoreCase("anonymousUser")) { return !showUpdateOnlyAdmin; } - Optional user = userRepository.findByUsername(authentication.getName()); if (user.isPresent() && showUpdateOnlyAdmin) { return "ROLE_ADMIN".equals(user.get().getRolesAsString()); } - return showUpdate; } } diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java index 357181966..69b06ec11 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -4,7 +4,6 @@ import java.util.Collection; import java.util.Set; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -20,9 +19,15 @@ import stirling.software.SPDF.repository.UserRepository; @Service public class CustomUserDetailsService implements UserDetailsService { - @Autowired private UserRepository userRepository; + private final UserRepository userRepository; - @Autowired private LoginAttemptService loginAttemptService; + private final LoginAttemptService loginAttemptService; + + public CustomUserDetailsService( + UserRepository userRepository, LoginAttemptService loginAttemptService) { + this.userRepository = userRepository; + this.loginAttemptService = loginAttemptService; + } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { @@ -33,16 +38,13 @@ public class CustomUserDetailsService implements UserDetailsService { () -> new UsernameNotFoundException( "No user found with username: " + username)); - if (loginAttemptService.isBlocked(username)) { throw new LockedException( "Your account has been locked due to too many failed login attempts."); } - if (!user.hasPassword()) { throw new IllegalArgumentException("Password must not be null"); } - return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java index babba64d2..ee1a5f489 100644 --- a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -5,7 +5,6 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -25,7 +24,11 @@ import stirling.software.SPDF.utils.RequestUriUtils; @Component public class FirstLoginFilter extends OncePerRequestFilter { - @Autowired @Lazy private UserService userService; + @Lazy private final UserService userService; + + public FirstLoginFilter(@Lazy UserService userService) { + this.userService = userService; + } @Override protected void doFilterInternal( @@ -34,16 +37,13 @@ public class FirstLoginFilter extends OncePerRequestFilter { String method = request.getMethod(); String requestURI = request.getRequestURI(); String contextPath = request.getContextPath(); - // Check if the request is for static resources boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI); - // If it's a static resource, just continue the filter chain and skip the logic below if (isStaticResource) { filterChain.doFilter(request, response); return; } - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { Optional user = userService.findByUsernameIgnoreCase(authentication.getName()); @@ -55,12 +55,10 @@ public class FirstLoginFilter extends OncePerRequestFilter { 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(), diff --git a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java index b47ba534a..02ac582ab 100644 --- a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java @@ -4,11 +4,7 @@ import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; +import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import stirling.software.SPDF.utils.RequestUriUtils; 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 3291021cb..54f291a6b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security; import java.io.IOException; import java.util.UUID; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; @@ -16,11 +15,20 @@ import stirling.software.SPDF.model.Role; @Slf4j public class InitialSecuritySetup { - @Autowired private UserService userService; + private final UserService userService; - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; - @Autowired private DatabaseBackupInterface databaseBackupHelper; + private final DatabaseBackupInterface databaseBackupHelper; + + public InitialSecuritySetup( + UserService userService, + ApplicationProperties applicationProperties, + DatabaseBackupInterface databaseBackupHelper) { + this.userService = userService; + this.applicationProperties = applicationProperties; + this.databaseBackupHelper = databaseBackupHelper; + } @PostConstruct public void init() throws IllegalArgumentException, IOException { diff --git a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java index 42886289e..088800d54 100644 --- a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java +++ b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java @@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import jakarta.annotation.PostConstruct; @@ -15,13 +14,20 @@ import stirling.software.SPDF.model.AttemptCounter; @Slf4j public class LoginAttemptService { - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; private int MAX_ATTEMPT; + private long ATTEMPT_INCREMENT_TIME; + private ConcurrentHashMap attemptsCache; + private boolean isBlockedEnabled = true; + public LoginAttemptService(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + @PostConstruct public void init() { MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount(); @@ -46,7 +52,6 @@ public class LoginAttemptService { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { return; } - AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); if (attemptCounter == null) { attemptCounter = new AttemptCounter(); @@ -67,20 +72,18 @@ public class LoginAttemptService { if (attemptCounter == null) { return false; } - return attemptCounter.getAttemptCount() >= MAX_ATTEMPT; } public int getRemainingAttempts(String key) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { - return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled + // Arbitrarily high number if tracking is disabled + return Integer.MAX_VALUE; } - AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); if (attemptCounter == null) { return MAX_ATTEMPT; } - return MAX_ATTEMPT - attemptCounter.getAttemptCount(); } } 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 66e6f0f5f..94422283c 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -4,7 +4,6 @@ import java.security.cert.X509Certificate; import java.util.*; import org.opensaml.saml.saml2.core.AuthnRequest; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -63,6 +62,7 @@ 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.PersistentLoginRepository; @Configuration @EnableWebSecurity @@ -71,38 +71,64 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl; @DependsOn("runningEE") public class SecurityConfiguration { - @Autowired private CustomUserDetailsService userDetailsService; + private final CustomUserDetailsService userDetailsService; + @Lazy private final UserService userService; + + @Qualifier("loginEnabled") + private final boolean loginEnabledValue; + + @Qualifier("runningEE") + private final boolean runningEE; + + private final ApplicationProperties applicationProperties; + private final UserAuthenticationFilter userAuthenticationFilter; + private final LoginAttemptService loginAttemptService; + private final FirstLoginFilter firstLoginFilter; + private final SessionPersistentRegistry sessionRegistry; + private final PersistentLoginRepository persistentLoginRepository; + + // // Only Dev test + // @Bean + // public WebSecurityCustomizer webSecurityCustomizer() { + // return (web) -> + // web.ignoring() + // .requestMatchers( + // "/css/**", "/images/**", "/js/**", "/**.svg", + // "/pdfjs-legacy/**"); + // } + public SecurityConfiguration( + PersistentLoginRepository persistentLoginRepository, + CustomUserDetailsService userDetailsService, + @Lazy UserService userService, + @Qualifier("loginEnabled") boolean loginEnabledValue, + @Qualifier("runningEE") boolean runningEE, + ApplicationProperties applicationProperties, + UserAuthenticationFilter userAuthenticationFilter, + LoginAttemptService loginAttemptService, + FirstLoginFilter firstLoginFilter, + SessionPersistentRegistry sessionRegistry) { + this.userDetailsService = userDetailsService; + this.userService = userService; + this.loginEnabledValue = loginEnabledValue; + this.runningEE = runningEE; + this.applicationProperties = applicationProperties; + this.userAuthenticationFilter = userAuthenticationFilter; + this.loginAttemptService = loginAttemptService; + this.firstLoginFilter = firstLoginFilter; + this.sessionRegistry = sessionRegistry; + this.persistentLoginRepository = persistentLoginRepository; + } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Autowired @Lazy private UserService userService; - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; - - @Autowired - @Qualifier("runningEE") - public boolean runningEE; - - @Autowired ApplicationProperties applicationProperties; - - @Autowired private UserAuthenticationFilter userAuthenticationFilter; - - @Autowired private LoginAttemptService loginAttemptService; - - @Autowired private FirstLoginFilter firstLoginFilter; - @Autowired private SessionPersistentRegistry sessionRegistry; - @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { http.csrf(csrf -> csrf.disable()); } - if (loginEnabledValue) { http.addFilterBefore( userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -117,13 +143,11 @@ public class SecurityConfiguration { csrf.ignoringRequestMatchers( request -> { String apiKey = request.getHeader("X-API-KEY"); - // If there's no API key, don't ignore CSRF // (return false) if (apiKey == null || apiKey.trim().isEmpty()) { return false; } - // Validate API key using existing UserService try { Optional user = @@ -152,7 +176,6 @@ public class SecurityConfiguration { .maxSessionsPreventsLogin(false) .sessionRegistry(sessionRegistry) .expiredUrl("/login?logout=true")); - http.authenticationProvider(daoAuthenticationProvider()); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); http.logout( @@ -161,18 +184,23 @@ public class SecurityConfiguration { .logoutSuccessHandler( new CustomLogoutSuccessHandler(applicationProperties)) .clearAuthentication(true) - .invalidateHttpSession(true) // Invalidate session + .invalidateHttpSession( // Invalidate session + true) .deleteCookies("JSESSIONID", "remember-me")); http.rememberMe( - rememberMeConfigurer -> - rememberMeConfigurer // Use the configurator directly + rememberMeConfigurer -> // Use the configurator directly + rememberMeConfigurer .tokenRepository(persistentTokenRepository()) - .tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days - .userDetailsService( - userDetailsService) // Your existing UserDetailsService - .useSecureCookie(true) // Enable secure cookie - .rememberMeParameter("remember-me") // Form parameter name - .rememberMeCookieName("remember-me") // Cookie name + .tokenValiditySeconds( // 14 days + 14 * 24 * 60 * 60) + .userDetailsService( // Your existing UserDetailsService + userDetailsService) + .useSecureCookie( // Enable secure cookie + true) + .rememberMeParameter( // Form parameter name + "remember-me") + .rememberMeCookieName( // Cookie name + "remember-me") .alwaysRemember(false)); http.authorizeHttpRequests( authz -> @@ -180,14 +208,12 @@ public class SecurityConfiguration { req -> { String uri = req.getRequestURI(); String contextPath = req.getContextPath(); - // Remove the context path from the URI String trimmedUri = uri.startsWith(contextPath) ? uri.substring( contextPath.length()) : uri; - return trimmedUri.startsWith("/login") || trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/saml2") @@ -205,7 +231,6 @@ public class SecurityConfiguration { .permitAll() .anyRequest() .authenticated()); - // Handle User/Password Logins if (applicationProperties.getSecurity().isUserPass()) { http.formLogin( @@ -221,27 +246,26 @@ public class SecurityConfiguration { .defaultSuccessUrl("/") .permitAll()); } - // Handle OAUTH2 Logins if (applicationProperties.getSecurity().isOauth2Activ()) { - http.oauth2Login( oauth2 -> oauth2.loginPage("/oauth2") + . /* This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser' is set as true, else login fails with an error message advising the same. */ - .successHandler( + successHandler( new CustomOAuth2AuthenticationSuccessHandler( loginAttemptService, applicationProperties, userService)) .failureHandler( new CustomOAuth2AuthenticationFailureHandler()) - // Add existing Authorities from the database - .userInfoEndpoint( + . // Add existing Authorities from the database + userInfoEndpoint( userInfoEndpoint -> userInfoEndpoint .oidcUserService( @@ -253,15 +277,14 @@ public class SecurityConfiguration { userAuthoritiesMapper())) .permitAll()); } - // Handle SAML - if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE + if (applicationProperties.getSecurity().isSaml2Activ()) { + // && runningEE // Configure the authentication provider OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider(); authenticationProvider.setResponseAuthenticationConverter( new CustomSaml2ResponseAuthenticationConverter(userService)); - http.authenticationProvider(authenticationProvider) .saml2Login( saml2 -> { @@ -287,7 +310,6 @@ public class SecurityConfiguration { } }); } - } else { // if (!applicationProperties.getSecurity().getCsrfDisabled()) { // CookieCsrfTokenRepository cookieRepo = @@ -302,7 +324,6 @@ public class SecurityConfiguration { // } http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); } - return http.build(); } @@ -313,17 +334,14 @@ public class SecurityConfiguration { matchIfMissing = false) public ClientRegistrationRepository clientRegistrationRepository() { List registrations = new ArrayList<>(); - githubClientRegistration().ifPresent(registrations::add); oidcClientRegistration().ifPresent(registrations::add); googleClientRegistration().ifPresent(registrations::add); keycloakClientRegistration().ifPresent(registrations::add); - if (registrations.isEmpty()) { log.error("At least one OAuth2 provider must be configured"); System.exit(1); } - return new InMemoryClientRegistrationRepository(registrations); } @@ -366,7 +384,6 @@ public class SecurityConfiguration { return Optional.empty(); } KeycloakProvider keycloak = client.getKeycloak(); - return keycloak != null && keycloak.isSettingsValid() ? Optional.of( ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) @@ -381,7 +398,6 @@ public class SecurityConfiguration { } private Optional githubClientRegistration() { - OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); if (oauth == null || !oauth.getEnabled()) { return Optional.empty(); @@ -443,19 +459,15 @@ public class SecurityConfiguration { matchIfMissing = false) public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); - X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); - Resource privateKeyResource = samlConf.getPrivateKey(); Resource certificateResource = samlConf.getSpCert(); - Saml2X509Credential signingCredential = new Saml2X509Credential( CertificateUtils.readPrivateKey(privateKeyResource), CertificateUtils.readCertificate(certificateResource), Saml2X509CredentialType.SIGNING); - RelyingPartyRegistration rp = RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) .signingX509Credentials(c -> c.add(signingCredential)) @@ -470,7 +482,6 @@ public class SecurityConfiguration { Saml2MessageBinding.POST) .wantAuthnRequestsSigned(true)) .build(); - return new InMemoryRelyingPartyRegistrationRepository(rp); } @@ -486,10 +497,8 @@ public class SecurityConfiguration { resolver.setAuthnRequestCustomizer( customizer -> { log.debug("Customizing SAML Authentication request"); - AuthnRequest authnRequest = customizer.getAuthnRequest(); log.debug("AuthnRequest ID: {}", authnRequest.getID()); - if (authnRequest.getID() == null) { authnRequest.setID("ARQ" + UUID.randomUUID().toString()); } @@ -500,16 +509,13 @@ public class SecurityConfiguration { authnRequest.getIssuer() != null ? authnRequest.getIssuer().getValue() : "null"); - HttpServletRequest request = customizer.getRequest(); - // Log HTTP request details log.debug("HTTP Request Method: {}", request.getMethod()); log.debug("Request URI: {}", request.getRequestURI()); log.debug("Request URL: {}", request.getRequestURL().toString()); log.debug("Query String: {}", request.getQueryString()); log.debug("Remote Address: {}", request.getRemoteAddr()); - // Log headers Collections.list(request.getHeaderNames()) .forEach( @@ -519,24 +525,20 @@ public class SecurityConfiguration { headerName, request.getHeader(headerName)); }); - // Log SAML specific parameters log.debug("SAML Request Parameters:"); log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest")); log.debug("RelayState: {}", request.getParameter("RelayState")); - // Log session debugrmation if exists if (request.getSession(false) != null) { log.debug("Session ID: {}", request.getSession().getId()); } - // Log any assertions consumer service details if present if (authnRequest.getAssertionConsumerServiceURL() != null) { log.debug( "AssertionConsumerServiceURL: {}", authnRequest.getAssertionConsumerServiceURL()); } - // Log NameID policy if present if (authnRequest.getNameIDPolicy() != null) { log.debug( @@ -566,12 +568,10 @@ public class SecurityConfiguration { GrantedAuthoritiesMapper userAuthoritiesMapper() { return (authorities) -> { Set mappedAuthorities = new HashSet<>(); - authorities.forEach( authority -> { // Add existing OAUTH2 Authorities mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); - // Add Authorities from database for existing user, if user is present. if (authority instanceof OAuth2UserAuthority oauth2Auth) { String useAsUsername = @@ -598,27 +598,18 @@ public class SecurityConfiguration { @Bean public IPRateLimitingFilter rateLimitingFilter() { - int maxRequestsPerIp = 1000000; // Example limit TODO add config level + // Example limit TODO add config level + int maxRequestsPerIp = 1000000; return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); } @Bean public PersistentTokenRepository persistentTokenRepository() { - return new JPATokenRepositoryImpl(); + return new JPATokenRepositoryImpl(persistentLoginRepository); } @Bean public boolean activSecurity() { return true; } - - // // Only Dev test - // @Bean - // public WebSecurityCustomizer webSecurityCustomizer() { - // return (web) -> - // web.ignoring() - // .requestMatchers( - // "/css/**", "/images/**", "/js/**", "/**.svg", - // "/pdfjs-legacy/**"); - // } } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index b17334941..13573349f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -5,14 +5,12 @@ import java.time.Duration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -31,13 +29,15 @@ import stirling.software.SPDF.model.Role; public class UserBasedRateLimitingFilter extends OncePerRequestFilter { private final Map apiBuckets = new ConcurrentHashMap<>(); + private final Map webBuckets = new ConcurrentHashMap<>(); - @Autowired private UserDetailsService userDetailsService; - - @Autowired @Qualifier("rateLimit") - public boolean rateLimit; + private final boolean rateLimit; + + public UserBasedRateLimitingFilter(@Qualifier("rateLimit") boolean rateLimit) { + this.rateLimit = rateLimit; + } @Override protected void doFilterInternal( @@ -48,21 +48,18 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } - String method = request.getMethod(); if (!"POST".equalsIgnoreCase(method)) { // If the request is not a POST, just pass it through without rate limiting filterChain.doFilter(request, response); return; } - String identifier = null; - // Check for API key in the request headers String apiKey = request.getHeader("X-API-KEY"); if (apiKey != null && !apiKey.trim().isEmpty()) { - identifier = - "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames + identifier = // Prefix to distinguish between API keys and usernames + "API_KEY_" + apiKey; } else { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { @@ -70,15 +67,12 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { identifier = userDetails.getUsername(); } } - // If neither API key nor an authenticated user is present, use IP address if (identifier == null) { identifier = request.getRemoteAddr(); } - Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); - if (request.getHeader("X-API-KEY") != null) { // It's an API call processRequest( @@ -123,7 +117,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { throws IOException, ServletException { Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); - if (probe.isConsumed()) { response.setHeader( "X-Rate-Limit-Remaining", 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 d5fea5942..5942f5fc4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.util.*; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -25,11 +24,7 @@ import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; -import stirling.software.SPDF.model.ApplicationProperties; -import stirling.software.SPDF.model.AuthenticationType; -import stirling.software.SPDF.model.Authority; -import stirling.software.SPDF.model.Role; -import stirling.software.SPDF.model.User; +import stirling.software.SPDF.model.*; import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.UserRepository; @@ -37,19 +32,36 @@ import stirling.software.SPDF.repository.UserRepository; @Slf4j public class UserService implements UserServiceInterface { - @Autowired private UserRepository userRepository; + private final UserRepository userRepository; - @Autowired private AuthorityRepository authorityRepository; + private final AuthorityRepository authorityRepository; - @Autowired private PasswordEncoder passwordEncoder; + private final PasswordEncoder passwordEncoder; - @Autowired private MessageSource messageSource; + private final MessageSource messageSource; - @Autowired private SessionPersistentRegistry sessionRegistry; + private final SessionPersistentRegistry sessionRegistry; - @Autowired DatabaseBackupInterface databaseBackupHelper; + private final DatabaseBackupInterface databaseBackupHelper; - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public UserService( + UserRepository userRepository, + AuthorityRepository authorityRepository, + PasswordEncoder passwordEncoder, + MessageSource messageSource, + SessionPersistentRegistry sessionRegistry, + DatabaseBackupInterface databaseBackupHelper, + ApplicationProperties applicationProperties) { + this.userRepository = userRepository; + this.authorityRepository = authorityRepository; + this.passwordEncoder = passwordEncoder; + this.messageSource = messageSource; + this.sessionRegistry = sessionRegistry; + this.databaseBackupHelper = databaseBackupHelper; + this.applicationProperties = applicationProperties; + } @Transactional public void migrateOauth2ToSSO() { @@ -84,13 +96,11 @@ public class UserService implements UserServiceInterface { if (!user.isPresent()) { throw new UsernameNotFoundException("API key is not valid"); } - // Convert the user into an Authentication object - return new UsernamePasswordAuthenticationToken( - user, // principal (typically the user) - null, // credentials (we don't expose the password or API key here) - getAuthorities(user.get()) // user's authorities (roles/permissions) - ); + return new UsernamePasswordAuthenticationToken( // principal (typically the user) + user, // credentials (we don't expose the password or API key here) + null, // user's authorities (roles/permissions) + getAuthorities(user.get())); } private Collection getAuthorities(User user) { @@ -104,7 +114,8 @@ public class UserService implements UserServiceInterface { String apiKey; do { apiKey = UUID.randomUUID().toString(); - } while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness + } while ( // Ensure uniqueness + userRepository.findByApiKey(apiKey).isPresent()); return apiKey; } @@ -118,7 +129,8 @@ public class UserService implements UserServiceInterface { } public User refreshApiKeyForUser(String username) { - return addApiKeyToUser(username); // reuse the add API key method for refreshing + // reuse the add API key method for refreshing + return addApiKeyToUser(username); } public String getApiKeyForUser(String username) { @@ -138,11 +150,11 @@ public class UserService implements UserServiceInterface { public Optional loadUserByApiKey(String apiKey) { Optional user = userRepository.findByApiKey(apiKey); - if (user.isPresent()) { return user; } - return null; // or throw an exception + // or throw an exception + return null; } public boolean validateApiKeyForUser(String username, String apiKey) { @@ -240,14 +252,12 @@ public class UserService implements UserServiceInterface { if (userOpt.isPresent()) { User user = userOpt.get(); Map settingsMap = user.getSettings(); - if (settingsMap == null) { settingsMap = new HashMap<>(); } settingsMap.clear(); settingsMap.putAll(updates); user.setSettings(settingsMap); - userRepository.save(user); databaseBackupHelper.exportDatabase(); } @@ -316,12 +326,9 @@ public class UserService implements UserServiceInterface { boolean isValidEmail = username.matches( "^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$"); - List notAllowedUserList = new ArrayList<>(); notAllowedUserList.add("ALL_USERS".toLowerCase()); - boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase()); - return (isValidSimpleUsername || isValidEmail) && !notAllowedUser; } @@ -374,7 +381,6 @@ public class UserService implements UserServiceInterface { public String getCurrentUsername() { Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - if (principal instanceof UserDetails) { return ((UserDetails) principal).getUsername(); } else if (principal instanceof OAuth2User) { @@ -397,7 +403,6 @@ public class UserService implements UserServiceInterface { } String username = "CUSTOM_API_USER"; Optional existingUser = findByUsernameIgnoreCase(username); - if (!existingUser.isPresent()) { // Create new user with API role User user = new User(); 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 85cef66f4..5db05bed7 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 @@ -6,12 +6,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.*; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; diff --git a/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java b/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java index 2ddc47e12..ad96573fc 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/ScheduledTasks.java @@ -2,14 +2,17 @@ package stirling.software.SPDF.config.security.database; import java.io.IOException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class ScheduledTasks { - @Autowired private DatabaseBackupHelper databaseBackupService; + private final DatabaseBackupHelper databaseBackupService; + + public ScheduledTasks(DatabaseBackupHelper databaseBackupService) { + this.databaseBackupService = databaseBackupService; + } @Scheduled(cron = "0 0 0 * * ?") public void performBackup() throws IOException { diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index 045b6375f..4c931e014 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -1,12 +1,7 @@ package stirling.software.SPDF.config.security.session; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.session.SessionInformation; diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java index aa920f84a..9710316e3 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionScheduled.java @@ -5,19 +5,22 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.security.core.session.SessionInformation; import org.springframework.stereotype.Component; @Component public class SessionScheduled { - @Autowired private SessionPersistentRegistry sessionPersistentRegistry; + + private final SessionPersistentRegistry sessionPersistentRegistry; + + public SessionScheduled(SessionPersistentRegistry sessionPersistentRegistry) { + this.sessionPersistentRegistry = sessionPersistentRegistry; + } @Scheduled(cron = "0 0/5 * * * ?") public void expireSessions() { Instant now = Instant.now(); - for (Object principal : sessionPersistentRegistry.getAllPrincipals()) { List sessionInformations = sessionPersistentRegistry.getAllSessions(principal, false); diff --git a/src/main/java/stirling/software/SPDF/controller/api/AdditionalLanguageJsController.java b/src/main/java/stirling/software/SPDF/controller/api/AdditionalLanguageJsController.java index 959e9ac45..41e173c78 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/AdditionalLanguageJsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/AdditionalLanguageJsController.java @@ -4,7 +4,6 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -18,35 +17,35 @@ import stirling.software.SPDF.service.LanguageService; @RequestMapping("/js") public class AdditionalLanguageJsController { - @Autowired private LanguageService languageService; + private final LanguageService languageService; + + public AdditionalLanguageJsController(LanguageService languageService) { + this.languageService = languageService; + } @Hidden @GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript") public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException { List supportedLanguages = languageService.getSupportedLanguages(); - response.setContentType("application/javascript"); PrintWriter writer = response.getWriter(); - // Erstelle das JavaScript dynamisch writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";"); - // Generiere die `getDetailedLanguageCode`-Funktion writer.println( """ - function getDetailedLanguageCode() { - const userLanguages = navigator.languages ? navigator.languages : [navigator.language]; - for (let lang of userLanguages) { - let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_'))); - if (matchedLang) { - return matchedLang; + function getDetailedLanguageCode() { + const userLanguages = navigator.languages ? navigator.languages : [navigator.language]; + for (let lang of userLanguages) { + let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_'))); + if (matchedLang) { + return matchedLang; + } + } + // Fallback + return "en_GB"; } - } - // Fallback - return "en_GB"; - } - """); - + """); writer.flush(); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java index efff6e92c..35e2bb603 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java @@ -8,18 +8,13 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import org.eclipse.jetty.http.HttpStatus; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.servlet.mvc.support.RedirectAttributes; @@ -38,7 +33,11 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper; @Tag(name = "Database", description = "Database APIs for backup, import, and management") public class DatabaseController { - @Autowired DatabaseBackupHelper databaseBackupHelper; + private final DatabaseBackupHelper databaseBackupHelper; + + public DatabaseController(DatabaseBackupHelper databaseBackupHelper) { + this.databaseBackupHelper = databaseBackupHelper; + } @Operation( summary = "Import a database backup file", @@ -50,13 +49,11 @@ public class DatabaseController { MultipartFile file, RedirectAttributes redirectAttributes) throws IOException { - if (file == null || file.isEmpty()) { redirectAttributes.addAttribute("error", "fileNullOrEmpty"); return "redirect:/database"; } log.info("Received file: {}", file.getOriginalFilename()); - Path tempTemplatePath = Files.createTempFile("backup_", ".sql"); try (InputStream in = file.getInputStream()) { Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING); @@ -82,11 +79,9 @@ public class DatabaseController { @Parameter(description = "Name of the file to import", required = true) @PathVariable String fileName) throws IOException { - if (fileName == null || fileName.isEmpty()) { return "redirect:/database?error=fileNullOrEmpty"; } - // Check if the file exists in the backup list boolean fileExists = databaseBackupHelper.getBackupList().stream() @@ -96,7 +91,6 @@ public class DatabaseController { return "redirect:/database?error=fileNotFound"; } log.info("Received file: {}", fileName); - if (databaseBackupHelper.importDatabaseFromUI(fileName)) { log.info("File {} imported to database", fileName); return "redirect:/database?infoMessage=importIntoDatabaseSuccessed"; @@ -112,7 +106,6 @@ public class DatabaseController { public String deleteFile( @Parameter(description = "Name of the file to delete", required = true) @PathVariable String fileName) { - if (fileName == null || fileName.isEmpty()) { throw new IllegalArgumentException("File must not be null or empty"); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 59b8bee32..1fb4d5d4d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api; -import java.awt.Color; +import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java b/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java index 92bd3bace..fdcb55a24 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java @@ -6,7 +6,10 @@ import java.io.IOException; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java index 034c6e23f..ce97c0bc0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SettingsController.java @@ -2,11 +2,12 @@ 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 org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; @@ -20,7 +21,11 @@ import stirling.software.SPDF.utils.GeneralUtils; @Hidden public class SettingsController { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public SettingsController(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @PostMapping("/update-enable-analytics") @Hidden @@ -32,7 +37,6 @@ public class SettingsController { } 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/SplitPdfByChaptersController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java index 37b4a4c26..e3bb9d35d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfByChaptersController.java @@ -49,6 +49,74 @@ public class SplitPdfByChaptersController { this.pdfMetadataService = pdfMetadataService; } + private static List extractOutlineItems( + PDDocument sourceDocument, + PDOutlineItem current, + List bookmarks, + PDOutlineItem nextParent, + int level, + int maxLevel) + throws Exception { + + while (current != null) { + + String currentTitle = current.getTitle().replace("/", ""); + int firstPage = + sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument)); + PDOutlineItem child = current.getFirstChild(); + PDOutlineItem nextSibling = current.getNextSibling(); + int endPage; + if (child != null && level < maxLevel) { + endPage = + sourceDocument + .getPages() + .indexOf(child.findDestinationPage(sourceDocument)); + } else if (nextSibling != null) { + endPage = + sourceDocument + .getPages() + .indexOf(nextSibling.findDestinationPage(sourceDocument)); + } else if (nextParent != null) { + + endPage = + sourceDocument + .getPages() + .indexOf(nextParent.findDestinationPage(sourceDocument)); + } else { + endPage = -2; + /* + happens when we have something like this: + Outline Item 2 + Outline Item 2.1 + Outline Item 2.1.1 + Outline Item 2.2 + Outline 2.2.1 + Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling + Outline Item 3 + */ + } + if (!bookmarks.isEmpty() + && bookmarks.get(bookmarks.size() - 1).getEndPage() == -2 + && firstPage + >= bookmarks + .get(bookmarks.size() - 1) + .getStartPage()) { // for handling the above-mentioned case + Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1); + previousBookmark.setEndPage(firstPage); + } + bookmarks.add(new Bookmark(currentTitle, firstPage, endPage)); + + // Recursively process children + if (child != null && level < maxLevel) { + extractOutlineItems( + sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel); + } + + current = nextSibling; + } + return bookmarks; + } + @PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data") @Operation( summary = "Split PDFs by Chapters", @@ -163,74 +231,6 @@ public class SplitPdfByChaptersController { return bookmarks; } - private static List extractOutlineItems( - PDDocument sourceDocument, - PDOutlineItem current, - List bookmarks, - PDOutlineItem nextParent, - int level, - int maxLevel) - throws Exception { - - while (current != null) { - - String currentTitle = current.getTitle().replace("/", ""); - int firstPage = - sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument)); - PDOutlineItem child = current.getFirstChild(); - PDOutlineItem nextSibling = current.getNextSibling(); - int endPage; - if (child != null && level < maxLevel) { - endPage = - sourceDocument - .getPages() - .indexOf(child.findDestinationPage(sourceDocument)); - } else if (nextSibling != null) { - endPage = - sourceDocument - .getPages() - .indexOf(nextSibling.findDestinationPage(sourceDocument)); - } else if (nextParent != null) { - - endPage = - sourceDocument - .getPages() - .indexOf(nextParent.findDestinationPage(sourceDocument)); - } else { - endPage = -2; - /* - happens when we have something like this: - Outline Item 2 - Outline Item 2.1 - Outline Item 2.1.1 - Outline Item 2.2 - Outline 2.2.1 - Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling - Outline Item 3 - */ - } - if (!bookmarks.isEmpty() - && bookmarks.get(bookmarks.size() - 1).getEndPage() == -2 - && firstPage - >= bookmarks - .get(bookmarks.size() - 1) - .getStartPage()) { // for handling the above-mentioned case - Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1); - previousBookmark.setEndPage(firstPage); - } - bookmarks.add(new Bookmark(currentTitle, firstPage, endPage)); - - // Recursively process children - if (child != null && level < maxLevel) { - extractOutlineItems( - sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel); - } - - current = nextSibling; - } - return bookmarks; - } - private Path createZipFile( List bookmarks, List splitDocumentsBoas) throws Exception { 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 c7d19f518..9a2202516 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -7,7 +7,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -18,11 +17,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.view.RedirectView; @@ -45,9 +40,14 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass; @Slf4j public class UserController { - @Autowired private UserService userService; + private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated"; + private final UserService userService; + private final SessionPersistentRegistry sessionRegistry; - @Autowired SessionPersistentRegistry sessionRegistry; + public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) { + this.userService = userService; + this.sessionRegistry = sessionRegistry; + } @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") @@ -75,36 +75,27 @@ public class UserController { HttpServletResponse response, RedirectAttributes redirectAttributes) throws IOException { - if (!userService.isUsernameValid(newUsername)) { return new RedirectView("/account?messageType=invalidUsername", true); } - if (principal == null) { return new RedirectView("/account?messageType=notAuthenticated", true); } - // The username MUST be unique when renaming Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { return new RedirectView("/account?messageType=userNotFound", true); } - User user = userOpt.get(); - if (user.getUsername().equals(newUsername)) { return new RedirectView("/account?messageType=usernameExists", true); } - if (!userService.isPasswordCorrect(user, currentPassword)) { return new RedirectView("/account?messageType=incorrectPassword", true); } - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { return new RedirectView("/account?messageType=usernameExists", true); } - if (newUsername != null && newUsername.length() > 0) { try { userService.changeUsername(user, newUsername); @@ -112,10 +103,8 @@ public class UserController { return new RedirectView("/account?messageType=invalidUsername", true); } } - // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); } @@ -132,24 +121,18 @@ public class UserController { if (principal == null) { return new RedirectView("/change-creds?messageType=notAuthenticated", true); } - Optional userOpt = userService.findByUsernameIgnoreCase(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { return new RedirectView("/change-creds?messageType=userNotFound", true); } - User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { return new RedirectView("/change-creds?messageType=incorrectPassword", true); } - userService.changePassword(user, newPassword); userService.changeFirstUse(user, false); // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); } @@ -166,24 +149,17 @@ public class UserController { if (principal == null) { return new RedirectView("/account?messageType=notAuthenticated", true); } - Optional userOpt = userService.findByUsernameIgnoreCase(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { return new RedirectView("/account?messageType=userNotFound", true); } - User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { return new RedirectView("/account?messageType=incorrectPassword", true); } - userService.changePassword(user, newPassword); - // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); } @@ -193,17 +169,14 @@ public class UserController { throws IOException { Map paramMap = request.getParameterMap(); Map updates = new HashMap<>(); - for (Map.Entry entry : paramMap.entrySet()) { updates.put(entry.getKey(), entry.getValue()[0]); } - log.debug("Processed updates: " + updates); - // Assuming you have a method in userService to update the settings for a user userService.updateUserSettings(principal.getName(), updates); - - return "redirect:/account"; // Redirect to a page of your choice after updating + // Redirect to a page of your choice after updating + return "redirect:/account"; } @PreAuthorize("hasRole('ROLE_ADMIN')") @@ -216,13 +189,10 @@ public class UserController { @RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) throws IllegalArgumentException, IOException { - if (!userService.isUsernameValid(username)) { return new RedirectView("/addUsers?messageType=invalidUsername", true); } - Optional userOpt = userService.findByUsernameIgnoreCase(username); - if (userOpt.isPresent()) { User user = userOpt.get(); if (user != null && user.getUsername().equalsIgnoreCase(username)) { @@ -243,7 +213,6 @@ public class UserController { // If the role ID is not valid, redirect with an error message return new RedirectView("/addUsers?messageType=invalidRole", true); } - if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { userService.saveUser(username, AuthenticationType.SSO, role); } else { @@ -252,9 +221,9 @@ public class UserController { } userService.saveUser(username, password, role, forceChange); } - return new RedirectView( - "/addUsers", true); // Redirect to account page after adding the user + "/addUsers", // Redirect to account page after adding the user + true); } @PreAuthorize("hasRole('ROLE_ADMIN')") @@ -264,9 +233,7 @@ public class UserController { @RequestParam(name = "role") String role, Authentication authentication) throws IOException { - Optional userOpt = userService.findByUsernameIgnoreCase(username); - if (!userOpt.isPresent()) { return new RedirectView("/addUsers?messageType=userNotFound", true); } @@ -275,7 +242,6 @@ public class UserController { } // Get the currently authenticated username String currentUsername = authentication.getName(); - // Check if the provided username matches the current session's username if (currentUsername.equalsIgnoreCase(username)) { return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true); @@ -292,11 +258,10 @@ public class UserController { return new RedirectView("/addUsers?messageType=invalidRole", true); } User user = userOpt.get(); - userService.changeRole(user, role); - return new RedirectView( - "/addUsers", true); // Redirect to account page after adding the user + "/addUsers", // Redirect to account page after adding the user + true); } @PreAuthorize("hasRole('ROLE_ADMIN')") @@ -306,9 +271,7 @@ public class UserController { @RequestParam("enabled") boolean enabled, Authentication authentication) throws IOException { - Optional userOpt = userService.findByUsernameIgnoreCase(username); - if (!userOpt.isPresent()) { return new RedirectView("/addUsers?messageType=userNotFound", true); } @@ -317,15 +280,12 @@ public class UserController { } // Get the currently authenticated username String currentUsername = authentication.getName(); - // Check if the provided username matches the current session's username if (currentUsername.equalsIgnoreCase(username)) { return new RedirectView("/addUsers?messageType=disabledCurrentUser", true); } User user = userOpt.get(); - userService.changeUserEnabled(user, enabled); - if (!enabled) { // Invalidate all sessions if the user is being disabled List principals = sessionRegistry.getAllPrincipals(); @@ -349,28 +309,24 @@ public class UserController { } } } - return new RedirectView( - "/addUsers", true); // Redirect to account page after adding the user + "/addUsers", // Redirect to account page after adding the user + true); } @PreAuthorize("hasRole('ROLE_ADMIN')") @PostMapping("/admin/deleteUser/{username}") public RedirectView deleteUser( @PathVariable("username") String username, Authentication authentication) { - if (!userService.usernameExistsIgnoreCase(username)) { return new RedirectView("/addUsers?messageType=deleteUsernameExists", true); } - // Get the currently authenticated username String currentUsername = authentication.getName(); - // Check if the provided username matches the current session's username if (currentUsername.equalsIgnoreCase(username)) { return new RedirectView("/addUsers?messageType=deleteCurrentUser", true); } - // Invalidate all sessions before deleting the user List sessionsInformations = sessionRegistry.getAllSessions(authentication.getPrincipal(), false); @@ -410,6 +366,4 @@ public class UserController { } return ResponseEntity.ok(apiKey); } - - private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated"; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index 36e29f275..50a251e4e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -33,6 +33,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RequestMapping("/api/v1/convert") public class ConvertOfficeController { + private final CustomPDDocumentFactory pdfDocumentFactory; + + @Autowired + public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) { + this.pdfDocumentFactory = pdfDocumentFactory; + } + public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { // Check for valid file extension String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); @@ -78,13 +85,6 @@ public class ConvertOfficeController { return fileExtension.matches(extensionPattern); } - private final CustomPDDocumentFactory pdfDocumentFactory; - - @Autowired - public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) { - this.pdfDocumentFactory = pdfDocumentFactory; - } - @PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @Operation( summary = "Convert a file to a PDF using LibreOffice", diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToBookController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToBookController.java index f1c672e12..181669724 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToBookController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToBookController.java @@ -6,7 +6,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -26,9 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; // @RequestMapping("/api/v1/convert") public class ConvertPDFToBookController { - @Autowired @Qualifier("bookAndHtmlFormatsInstalled") - private boolean bookAndHtmlFormatsInstalled; + private final boolean bookAndHtmlFormatsInstalled; + + public ConvertPDFToBookController( + @Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) { + this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled; + } @PostMapping(consumes = "multipart/form-data", value = "/pdf/book") @Operation( @@ -39,16 +42,13 @@ public class ConvertPDFToBookController { public ResponseEntity HtmlToPdf(@ModelAttribute PdfToBookRequest request) throws Exception { MultipartFile fileInput = request.getFileInput(); - if (!bookAndHtmlFormatsInstalled) { throw new IllegalArgumentException( "bookAndHtmlFormatsInstalled flag is False, this functionality is not available"); } - if (fileInput == null) { throw new IllegalArgumentException("Please provide a file for conversion."); } - // Validate the output format String outputFormat = request.getOutputFormat().toLowerCase(); List allowedFormats = @@ -58,28 +58,24 @@ public class ConvertPDFToBookController { if (!allowedFormats.contains(outputFormat)) { throw new IllegalArgumentException("Invalid output format: " + outputFormat); } - byte[] outputFileBytes; List command = new ArrayList<>(); Path tempOutputFile = Files.createTempFile( - "output_", - "." + outputFormat); // Use the output format for the file extension + "output_", // Use the output format for the file extension + "." + outputFormat); Path tempInputFile = null; - try { // Create temp input file from the provided PDF - tempInputFile = Files.createTempFile("input_", ".pdf"); // Assuming input is always PDF + // Assuming input is always PDF + tempInputFile = Files.createTempFile("input_", ".pdf"); Files.write(tempInputFile, fileInput.getBytes()); - command.add("ebook-convert"); command.add(tempInputFile.toString()); command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE) .runCommandWithOutputHandling(command); - outputFileBytes = Files.readAllBytes(tempOutputFile); } finally { // Clean up temporary files @@ -88,13 +84,12 @@ public class ConvertPDFToBookController { } Files.deleteIfExists(tempOutputFile); } - String outputFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename()) .replaceFirst("[.][^.]+$", "") + "." - + outputFormat; // Remove file extension and append .pdf - + + // Remove file extension and append .pdf + outputFormat; return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename); } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index 8f8b13f0a..6625c6f5d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -46,16 +46,6 @@ public class AutoRenameController { PDDocument document = Loader.loadPDF(file.getBytes()); PDFTextStripper reader = new PDFTextStripper() { - class LineInfo { - String text; - float fontSize; - - LineInfo(String text, float fontSize) { - this.text = text; - this.fontSize = fontSize; - } - } - List lineInfos = new ArrayList<>(); StringBuilder lineBuilder = new StringBuilder(); float lastY = -1; @@ -122,6 +112,16 @@ public class AutoRenameController { .text) : null); } + + class LineInfo { + String text; + float fontSize; + + LineInfo(String text, float fontSize) { + this.text = text; + this.fontSize = fontSize; + } + } }; String header = reader.getText(document); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index 23c9ac284..9c6cbf9bf 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -23,12 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.google.zxing.BinaryBitmap; -import com.google.zxing.LuminanceSource; -import com.google.zxing.MultiFormatReader; -import com.google.zxing.NotFoundException; -import com.google.zxing.PlanarYUVLuminanceSource; -import com.google.zxing.Result; +import com.google.zxing.*; import com.google.zxing.common.HybridBinarizer; import io.github.pixee.security.Filenames; @@ -56,6 +51,52 @@ public class AutoSplitPdfController { this.pdfDocumentFactory = pdfDocumentFactory; } + private static String decodeQRCode(BufferedImage bufferedImage) { + LuminanceSource source; + + if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { + byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); + source = + new PlanarYUVLuminanceSource( + pixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); + } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { + int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); + byte[] newPixels = new byte[pixels.length]; + for (int i = 0; i < pixels.length; i++) { + newPixels[i] = (byte) (pixels[i] & 0xff); + } + source = + new PlanarYUVLuminanceSource( + newPixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); + } else { + throw new IllegalArgumentException( + "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); + } + + BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); + + try { + Result result = new MultiFormatReader().decode(bitmap); + return result.getText(); + } catch (NotFoundException e) { + return null; // there is no QR code in the image + } + } + @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @Operation( summary = "Auto split PDF pages into separate documents", @@ -154,50 +195,4 @@ public class AutoSplitPdfController { } } } - - private static String decodeQRCode(BufferedImage bufferedImage) { - LuminanceSource source; - - if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { - byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); - source = - new PlanarYUVLuminanceSource( - pixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { - int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); - byte[] newPixels = new byte[pixels.length]; - for (int i = 0; i < pixels.length; i++) { - newPixels[i] = (byte) (pixels[i] & 0xff); - } - source = - new PlanarYUVLuminanceSource( - newPixels, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - 0, - 0, - bufferedImage.getWidth(), - bufferedImage.getHeight(), - false); - } else { - throw new IllegalArgumentException( - "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); - } - - BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); - - try { - Result result = new MultiFormatReader().decode(bitmap); - return result.getText(); - } catch (NotFoundException e) { - return null; // there is no QR code in the image - } - } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index f88432ddb..1f3407d33 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -47,6 +47,32 @@ public class BlankPageController { this.pdfDocumentFactory = pdfDocumentFactory; } + public static boolean isBlankImage( + BufferedImage image, int threshold, double whitePercent, int blurSize) { + if (image == null) { + log.info("Error: Image is null"); + return false; + } + + // Convert to binary image based on the threshold + int whitePixels = 0; + int totalPixels = image.getWidth() * image.getHeight(); + + for (int i = 0; i < image.getHeight(); i++) { + for (int j = 0; j < image.getWidth(); j++) { + int color = image.getRGB(j, i) & 0xFF; + if (color >= 255 - threshold) { + whitePixels++; + } + } + } + + double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100; + log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage)); + + return whitePixelPercentage >= whitePercent; + } + @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @Operation( summary = "Remove blank pages from a PDF file", @@ -143,30 +169,4 @@ public class BlankPageController { zos.closeEntry(); } } - - public static boolean isBlankImage( - BufferedImage image, int threshold, double whitePercent, int blurSize) { - if (image == null) { - log.info("Error: Image is null"); - return false; - } - - // Convert to binary image based on the threshold - int whitePixels = 0; - int totalPixels = image.getWidth() * image.getHeight(); - - for (int i = 0; i < image.getHeight(); i++) { - for (int j = 0; j < image.getWidth(); j++) { - int color = image.getRGB(j, i) & 0xFF; - if (color >= 255 - threshold) { - whitePixels++; - } - } - } - - double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100; - log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage)); - - return whitePixelPercentage >= whitePercent; - } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index 39bf37b0f..a012716a3 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api.misc; -import java.awt.Image; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.ByteArrayOutputStream; import java.nio.file.Files; diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index 2b83e618f..6bcc8003b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -42,6 +42,8 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class ExtractImageScansController { + private static final String REPLACEFIRST = "[.][^.]+$"; + @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @Operation( summary = "Extract image scans from an input file", @@ -221,6 +223,4 @@ public class ExtractImageScansController { }); } } - - private static final String REPLACEFIRST = "[.][^.]+$"; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java index f8e026a82..ca4cbd8fd 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java @@ -1,19 +1,10 @@ package stirling.software.SPDF.controller.api.misc; -import java.awt.AlphaComposite; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.GradientPaint; -import java.awt.Graphics2D; -import java.awt.RenderingHints; +import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; -import java.awt.image.AffineTransformOp; -import java.awt.image.BufferedImage; -import java.awt.image.BufferedImageOp; -import java.awt.image.ConvolveOp; -import java.awt.image.Kernel; +import java.awt.image.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.security.SecureRandom; diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index a26c48390..2eec95471 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -13,11 +13,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import io.github.pixee.security.Filenames; diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index f503c107a..09c6576ee 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -1,18 +1,10 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.*; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -24,7 +16,6 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.text.PDFTextStripper; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; @@ -48,12 +39,14 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory; @Slf4j public class OCRController { - @Autowired private ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; private final CustomPDDocumentFactory pdfDocumentFactory; - @Autowired - public OCRController(CustomPDDocumentFactory pdfDocumentFactory) { + public OCRController( + ApplicationProperties applicationProperties, + CustomPDDocumentFactory pdfDocumentFactory) { + this.applicationProperties = applicationProperties; this.pdfDocumentFactory = pdfDocumentFactory; } @@ -78,13 +71,11 @@ public class OCRController { MultipartFile inputFile = request.getFileInput(); List languages = request.getLanguages(); String ocrType = request.getOcrType(); - Path tempDir = Files.createTempDirectory("ocr_process"); Path tempInputFile = tempDir.resolve("input.pdf"); Path tempOutputDir = tempDir.resolve("output"); Path tempImagesDir = tempDir.resolve("images"); Path finalOutputFile = tempDir.resolve("final_output.pdf"); - Files.createDirectories(tempOutputDir); Files.createDirectories(tempImagesDir); Process process = null; @@ -93,39 +84,32 @@ public class OCRController { inputFile.transferTo(tempInputFile.toFile()); PDFMergerUtility merger = new PDFMergerUtility(); merger.setDestinationFileName(finalOutputFile.toString()); - try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) { PDFRenderer pdfRenderer = new PDFRenderer(document); int pageCount = document.getNumberOfPages(); - for (int pageNum = 0; pageNum < pageCount; pageNum++) { PDPage page = document.getPage(pageNum); boolean hasText = false; - // Check for existing text try (PDDocument tempDoc = new PDDocument()) { tempDoc.addPage(page); PDFTextStripper stripper = new PDFTextStripper(); hasText = !stripper.getText(tempDoc).trim().isEmpty(); } - boolean shouldOcr = switch (ocrType) { case "skip-text" -> !hasText; case "force-ocr" -> true; default -> true; }; - Path pageOutputPath = tempOutputDir.resolve(String.format("page_%d.pdf", pageNum)); - if (shouldOcr) { // Convert page to image BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300); Path imagePath = tempImagesDir.resolve(String.format("page_%d.png", pageNum)); ImageIO.write(image, "png", imagePath.toFile()); - // Build OCR command List command = new ArrayList<>(); command.add("tesseract"); @@ -136,11 +120,10 @@ public class OCRController { .toString()); command.add("-l"); command.add(String.join("+", languages)); - command.add("pdf"); // Always output PDF - + // Always output PDF + command.add("pdf"); ProcessBuilder pb = new ProcessBuilder(command); process = pb.start(); - // Capture any error output try (BufferedReader reader = new BufferedReader( @@ -150,13 +133,11 @@ public class OCRController { log.debug("Tesseract: {}", line); } } - int exitCode = process.waitFor(); if (exitCode != 0) { throw new RuntimeException( "Tesseract failed with exit code: " + exitCode); } - // Add OCR'd PDF to merger merger.addSource(pageOutputPath.toFile()); } else { @@ -169,29 +150,24 @@ public class OCRController { } } } - // Merge all pages into final PDF merger.mergeDocuments(null); - // Read the final PDF file byte[] pdfContent = Files.readAllBytes(finalOutputFile); String outputFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()) .replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; - return ResponseEntity.ok() .header( "Content-Disposition", "attachment; filename=\"" + outputFilename + "\"") .contentType(MediaType.APPLICATION_PDF) .body(pdfContent); - } finally { if (process != null) { process.destroy(); } - // Clean up temporary files deleteDirectory(tempDir); } @@ -203,17 +179,14 @@ public class OCRController { log.warn("File {} does not exist, skipping", file); return; } - try (FileInputStream fis = new FileInputStream(file)) { ZipEntry zipEntry = new ZipEntry(filename); zipOut.putNextEntry(zipEntry); - byte[] buffer = new byte[1024]; int length; while ((length = fis.read(buffer)) >= 0) { zipOut.write(buffer, 0, length); } - zipOut.closeEntry(); } } 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 8f993abd0..059e70513 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 @@ -1,7 +1,6 @@ package stirling.software.SPDF.controller.api.misc; -import java.awt.Graphics; -import java.awt.Graphics2D; +import java.awt.*; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Printable; diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java index 9bf7fec52..30a7540ee 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api.misc; -import java.awt.Color; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java index 2c5b9fac2..4992c2f62 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java @@ -30,17 +30,24 @@ public class ApiDocService { private final Map apiDocumentation = new HashMap<>(); - @Autowired private ServletContext servletContext; + private final ServletContext servletContext; + private final UserServiceInterface userService; + Map> outputToFileTypes = new HashMap<>(); + JsonNode apiDocsJsonRootNode; + + public ApiDocService( + ServletContext servletContext, + @Autowired(required = false) UserServiceInterface userService) { + this.servletContext = servletContext; + this.userService = userService; + } private String getApiDocsUrl() { String contextPath = servletContext.getContextPath(); String port = SPdfApplication.getStaticPort(); - return "http://localhost:" + port + contextPath + "/v1/api-docs"; } - Map> outputToFileTypes = new HashMap<>(); - public List getExtensionTypes(boolean output, String operationName) { if (outputToFileTypes.size() == 0) { outputToFileTypes.put("PDF", Arrays.asList("pdf")); @@ -64,14 +71,12 @@ public class ApiDocService { "BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx")); // type. } - if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { loadApiDocumentation(); } if (!apiDocumentation.containsKey(operationName)) { return null; } - ApiEndpoint endpoint = apiDocumentation.get(operationName); String description = endpoint.getDescription(); Pattern pattern = null; @@ -90,16 +95,11 @@ public class ApiDocService { return null; } - @Autowired(required = false) - private UserServiceInterface userService; - private String getApiKeyForUser() { if (userService == null) return ""; return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); } - JsonNode apiDocsJsonRootNode; - // @EventListener(ApplicationReadyEvent.class) private synchronized void loadApiDocumentation() { String apiDocsJson = ""; @@ -110,15 +110,12 @@ public class ApiDocService { headers.set("X-API-KEY", apiKey); } HttpEntity entity = new HttpEntity<>(headers); - RestTemplate restTemplate = new RestTemplate(); ResponseEntity response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); apiDocsJson = response.getBody(); - ObjectMapper mapper = new ObjectMapper(); apiDocsJsonRootNode = mapper.readTree(apiDocsJson); - JsonNode paths = apiDocsJsonRootNode.path("paths"); paths.fields() .forEachRemaining( @@ -155,19 +152,15 @@ public class ApiDocService { if (!apiDocumentation.containsKey(operationName)) { return false; } - ApiEndpoint endpoint = apiDocumentation.get(operationName); String description = endpoint.getDescription(); - Pattern pattern = Pattern.compile("Type:(\\w+)"); Matcher matcher = pattern.matcher(description); if (matcher.find()) { String type = matcher.group(1); return type.startsWith("MI"); } - return false; } } - // Model class for API Endpoint diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index 890ae7231..d91ad7e3d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -8,7 +8,6 @@ import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -37,17 +36,27 @@ import stirling.software.SPDF.utils.WebResponseUtils; public class PipelineController { final String watchedFoldersDir = "./pipeline/watchedFolders/"; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; - @Autowired PipelineProcessor processor; - @Autowired ApplicationProperties applicationProperties; + private final PipelineProcessor processor; - @Autowired private ObjectMapper objectMapper; + private final ApplicationProperties applicationProperties; + + private final ObjectMapper objectMapper; + + public PipelineController( + PipelineProcessor processor, + ApplicationProperties applicationProperties, + ObjectMapper objectMapper) { + this.processor = processor; + this.applicationProperties = applicationProperties; + this.objectMapper = objectMapper; + } @PostMapping("/handleData") public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException { - MultipartFile[] files = request.getFileInput(); String jsonString = request.getJson(); if (files == null) { @@ -68,26 +77,21 @@ public class PipelineController { byte[] bytes = new byte[(int) singleFile.contentLength()]; is.read(bytes); is.close(); - log.info("Returning single file response..."); return WebResponseUtils.bytesToWebResponse( bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); } else if (outputFiles == null) { return null; } - // Create a ByteArrayOutputStream to hold the zip ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zipOut = new ZipOutputStream(baos); - // A map to keep track of filenames and their counts Map filenameCount = new HashMap<>(); - // Loop through each file and add it to the zip for (Resource file : outputFiles) { String originalFilename = file.getFilename(); String filename = originalFilename; - // Check if the filename already exists, and modify it if necessary if (filenameCount.containsKey(originalFilename)) { int count = filenameCount.get(originalFilename); @@ -98,24 +102,18 @@ public class PipelineController { } else { filenameCount.put(originalFilename, 1); } - ZipEntry zipEntry = new ZipEntry(filename); zipOut.putNextEntry(zipEntry); - // Read the file into a byte array InputStream is = file.getInputStream(); byte[] bytes = new byte[(int) file.contentLength()]; is.read(bytes); - // Write the bytes of the file to the zip zipOut.write(bytes, 0, bytes.length); zipOut.closeEntry(); - is.close(); } - zipOut.close(); - log.info("Returning zipped file response..."); return WebResponseUtils.boasToWebResponse( baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java index a46ea8ae7..2595c2fec 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -16,7 +16,6 @@ import java.util.List; import java.util.Optional; import java.util.stream.Stream; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; @@ -34,19 +33,31 @@ import stirling.software.SPDF.utils.FileMonitor; @Slf4j public class PipelineDirectoryProcessor { - @Autowired private ObjectMapper objectMapper; - @Autowired private ApiDocService apiDocService; - @Autowired PipelineProcessor processor; - @Autowired FileMonitor fileMonitor; + private final ObjectMapper objectMapper; - final String watchedFoldersDir; - final String finishedFoldersDir; + private final ApiDocService apiDocService; + + private final PipelineProcessor processor; + + private final FileMonitor fileMonitor; + + private final String watchedFoldersDir; + + private final String finishedFoldersDir; public PipelineDirectoryProcessor( + ObjectMapper objectMapper, + ApiDocService apiDocService, @Qualifier("watchedFoldersDir") String watchedFoldersDir, - @Qualifier("finishedFoldersDir") String finishedFoldersDir) { + @Qualifier("finishedFoldersDir") String finishedFoldersDir, + PipelineProcessor processor, + FileMonitor fileMonitor) { + this.objectMapper = objectMapper; + this.apiDocService = apiDocService; this.watchedFoldersDir = watchedFoldersDir; this.finishedFoldersDir = finishedFoldersDir; + this.processor = processor; + this.fileMonitor = fileMonitor; } @Scheduled(fixedRate = 60000) @@ -81,13 +92,11 @@ public class PipelineDirectoryProcessor { public void handleDirectory(Path dir) throws IOException { log.info("Handling directory: {}", dir); Path processingDir = createProcessingDirectory(dir); - Optional jsonFileOptional = findJsonFile(dir); if (!jsonFileOptional.isPresent()) { log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir); return; } - Path jsonFile = jsonFileOptional.get(); PipelineConfig config = readAndParseJson(jsonFile); processPipelineOperations(dir, processingDir, jsonFile, config); @@ -166,13 +175,11 @@ public class PipelineDirectoryProcessor { private Path resolveUniqueFilePath(Path directory, String originalFileName) { Path filePath = directory.resolve(originalFileName); int counter = 1; - while (Files.exists(filePath)) { String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")"); filePath = directory.resolve(newName); counter++; } - return filePath; } @@ -211,17 +218,14 @@ public class PipelineDirectoryProcessor { for (Resource resource : resources) { String outputFileName = createOutputFileName(resource, config); Path outputPath = determineOutputPath(config, dir); - if (!Files.exists(outputPath)) { Files.createDirectories(outputPath); log.info("Created directory: {}", outputPath); } - Path outputFile = outputPath.resolve(outputFileName); try (OutputStream os = new FileOutputStream(outputFile.toFile())) { os.write(((ByteArrayResource) resource).getByteArray()); } - log.info("File moved and renamed to {}", outputFile); } } @@ -230,7 +234,6 @@ public class PipelineDirectoryProcessor { String resourceName = resource.getFilename(); String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); - String outputFileName = config.getOutputPattern() .replace("{filename}", baseName) @@ -245,7 +248,6 @@ public class PipelineDirectoryProcessor { .format(DateTimeFormatter.ofPattern("HHmmss"))) + "." + extension; - return outputFileName; } @@ -255,7 +257,6 @@ public class PipelineDirectoryProcessor { .replace("{outputFolder}", finishedFoldersDir) .replace("{folderName}", dir.toString()) .replaceAll("\\\\?watchedFolders", ""); - return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java index cfa6fbec8..c9b741a0a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -1,10 +1,6 @@ package stirling.software.SPDF.controller.api.pipeline; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.PrintStream; +import java.io.*; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -22,12 +18,7 @@ import java.util.zip.ZipInputStream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -48,12 +39,39 @@ import stirling.software.SPDF.model.Role; @Slf4j public class PipelineProcessor { - @Autowired private ApiDocService apiDocService; + private final ApiDocService apiDocService; - @Autowired(required = false) - private UserServiceInterface userService; + private final UserServiceInterface userService; - @Autowired private ServletContext servletContext; + private final ServletContext servletContext; + + public PipelineProcessor( + ApiDocService apiDocService, + @Autowired(required = false) UserServiceInterface userService, + ServletContext servletContext) { + this.apiDocService = apiDocService; + this.userService = userService; + this.servletContext = servletContext; + } + + public static String removeTrailingNaming(String filename) { + // Splitting filename into name and extension + int dotIndex = filename.lastIndexOf("."); + if (dotIndex == -1) { + // No extension found + return filename; + } + String name = filename.substring(0, dotIndex); + String extension = filename.substring(dotIndex); + // Finding the last underscore + int underscoreIndex = name.lastIndexOf("_"); + if (underscoreIndex == -1) { + // No underscore found + return filename; + } + // Removing the last part and reattaching the extension + return name.substring(0, underscoreIndex) + extension; + } private String getApiKeyForUser() { if (userService == null) return ""; @@ -63,22 +81,17 @@ public class PipelineProcessor { private String getBaseUrl() { String contextPath = servletContext.getContextPath(); String port = SPdfApplication.getStaticPort(); - return "http://localhost:" + port + contextPath + "/"; } List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) throws Exception { - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); PrintStream logPrintStream = new PrintStream(logStream); - boolean hasErrors = false; - for (PipelineOperation pipelineOperation : config.getOperations()) { String operation = pipelineOperation.getOperation(); boolean isMultiInputOperation = apiDocService.isMultiInput(operation); - log.info( "Running operation: {} isMultiInputOperation {}", operation, @@ -89,9 +102,7 @@ public class PipelineProcessor { inputFileTypes = new ArrayList(Arrays.asList("ALL")); } // List outputFileTypes = apiDocService.getExtensionTypes(true, operation); - String url = getBaseUrl() + operation; - List newOutputFiles = new ArrayList<>(); if (!isMultiInputOperation) { for (Resource file : outputFiles) { @@ -101,7 +112,6 @@ public class PipelineProcessor { hasInputFileType = true; MultiValueMap body = new LinkedMultiValueMap<>(); body.add("fileInput", file); - for (Entry entry : parameters.entrySet()) { if (entry.getValue() instanceof List) { List list = (List) entry.getValue(); @@ -112,9 +122,7 @@ public class PipelineProcessor { body.add(entry.getKey(), entry.getValue()); } } - ResponseEntity response = sendWebRequest(url, body); - // If the operation is filter and the response body is null or empty, // skip // this @@ -125,7 +133,6 @@ public class PipelineProcessor { log.info("Skipping file due to failing {}", operation); continue; } - if (!response.getStatusCode().equals(HttpStatus.OK)) { logPrintStream.println("Error: " + response.getBody()); hasErrors = true; @@ -134,7 +141,6 @@ public class PipelineProcessor { processOutputFiles(operation, response, newOutputFiles); } } - if (!hasInputFileType) { logPrintStream.println( "No files with extension " @@ -144,7 +150,6 @@ public class PipelineProcessor { hasErrors = true; } } - } else { // Filter and collect all files that match the inputFileExtension List matchingFiles; @@ -160,17 +165,14 @@ public class PipelineProcessor { .anyMatch(file.getFilename()::endsWith)) .collect(Collectors.toList()); } - // Check if there are matching files if (!matchingFiles.isEmpty()) { // Create a new MultiValueMap for the request body MultiValueMap body = new LinkedMultiValueMap<>(); - // Add all matching files to the body for (Resource file : matchingFiles) { body.add("fileInput", file); } - for (Entry entry : parameters.entrySet()) { if (entry.getValue() instanceof List) { List list = (List) entry.getValue(); @@ -181,9 +183,7 @@ public class PipelineProcessor { body.add(entry.getKey(), entry.getValue()); } } - ResponseEntity response = sendWebRequest(url, body); - // Handle the response if (response.getStatusCode().equals(HttpStatus.OK)) { processOutputFiles(operation, response, newOutputFiles); @@ -208,48 +208,22 @@ public class PipelineProcessor { if (hasErrors) { log.error("Errors occurred during processing. Log: {}", logStream.toString()); } - return outputFiles; } private ResponseEntity sendWebRequest(String url, MultiValueMap body) { RestTemplate restTemplate = new RestTemplate(); - // Set up headers, including API key - HttpHeaders headers = new HttpHeaders(); String apiKey = getApiKeyForUser(); headers.add("X-API-KEY", apiKey); headers.setContentType(MediaType.MULTIPART_FORM_DATA); - // Create HttpEntity with the body and headers HttpEntity> entity = new HttpEntity<>(body, headers); - // Make the request to the REST endpoint return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); } - public static String removeTrailingNaming(String filename) { - // Splitting filename into name and extension - int dotIndex = filename.lastIndexOf("."); - if (dotIndex == -1) { - // No extension found - return filename; - } - String name = filename.substring(0, dotIndex); - String extension = filename.substring(dotIndex); - - // Finding the last underscore - int underscoreIndex = name.lastIndexOf("_"); - if (underscoreIndex == -1) { - // No underscore found - return filename; - } - - // Removing the last part and reattaching the extension - return name.substring(0, underscoreIndex) + extension; - } - private List processOutputFiles( String operation, ResponseEntity response, List newOutputFiles) throws IOException { @@ -259,13 +233,11 @@ public class PipelineProcessor { // If the operation is "auto-rename", generate a new filename. // This is a simple example of generating a filename using current timestamp. // Modify as per your needs. - newFilename = extractFilename(response); } else { // Otherwise, keep the original filename. newFilename = removeTrailingNaming(extractFilename(response)); } - // Check if the response body is a zip file if (isZip(response.getBody())) { // Unzip the file and add all the files to the new output files @@ -273,6 +245,7 @@ public class PipelineProcessor { } else { Resource outputResource = new ByteArrayResource(response.getBody()) { + @Override public String getFilename() { return newFilename; @@ -280,16 +253,14 @@ public class PipelineProcessor { }; newOutputFiles.add(outputResource); } - return newOutputFiles; } public String extractFilename(ResponseEntity response) { - String filename = "default-filename.ext"; // Default filename if not found - + // Default filename if not found + String filename = "default-filename.ext"; HttpHeaders headers = response.getHeaders(); String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); - if (contentDisposition != null && !contentDisposition.isEmpty()) { String[] parts = contentDisposition.split(";"); for (String part : parts) { @@ -297,12 +268,10 @@ public class PipelineProcessor { // Extracts filename and removes quotes if present filename = part.split("=")[1].trim().replace("\"", ""); filename = URLDecoder.decode(filename, StandardCharsets.UTF_8); - break; } } } - return filename; } @@ -311,16 +280,15 @@ public class PipelineProcessor { log.info("No files"); return null; } - List outputFiles = new ArrayList<>(); - for (File file : files) { Path path = Paths.get(file.getAbsolutePath()); - log.info("Reading file: " + path); // debug statement - + // debug statement + log.info("Reading file: " + path); if (Files.exists(path)) { Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { + @Override public String getFilename() { return file.getName(); @@ -340,12 +308,11 @@ public class PipelineProcessor { log.info("No files"); return null; } - List outputFiles = new ArrayList<>(); - for (MultipartFile file : files) { Resource fileResource = new ByteArrayResource(file.getBytes()) { + @Override public String getFilename() { return Filenames.toSimpleFileName(file.getOriginalFilename()); @@ -361,7 +328,6 @@ public class PipelineProcessor { if (data == null || data.length < 4) { return false; } - // Check the first four bytes of the data against the standard zip magic number return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; } @@ -369,29 +335,25 @@ public class PipelineProcessor { private List unzip(byte[] data) throws IOException { log.info("Unzipping data of length: {}", data.length); List unzippedFiles = new ArrayList<>(); - try (ByteArrayInputStream bais = new ByteArrayInputStream(data); ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) { - ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int count; - while ((count = zis.read(buffer)) != -1) { baos.write(buffer, 0, count); } - final String filename = entry.getName(); Resource fileResource = new ByteArrayResource(baos.toByteArray()) { + @Override public String getFilename() { return filename; } }; - // If the unzipped file is a zip file, unzip it if (isZip(baos.toByteArray())) { log.info("File {} is a zip file. Unzipping...", filename); @@ -401,7 +363,6 @@ public class PipelineProcessor { } } } - log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); return unzippedFiles; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 2058b55a2..68dca8b81 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,20 +1,9 @@ package stirling.software.SPDF.controller.api.security; -import java.awt.Color; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; +import java.awt.*; +import java.io.*; import java.nio.file.Files; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Security; -import java.security.UnrecoverableKeyException; +import java.security.*; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -91,6 +80,151 @@ public class CertSignController { Security.addProvider(new BouncyCastleProvider()); } + private final CustomPDDocumentFactory pdfDocumentFactory; + + @Autowired + public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) { + this.pdfDocumentFactory = pdfDocumentFactory; + } + + private static void sign( + CustomPDDocumentFactory pdfDocumentFactory, + byte[] input, + OutputStream output, + CreateSignature instance, + Boolean showSignature, + Integer pageNumber, + String name, + String location, + String reason, + Boolean showLogo) { + try (PDDocument doc = pdfDocumentFactory.load(input)) { + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + signature.setSignDate(Calendar.getInstance()); + + if (showSignature) { + SignatureOptions signatureOptions = new SignatureOptions(); + signatureOptions.setVisualSignature( + instance.createVisibleSignature(doc, signature, pageNumber, showLogo)); + signatureOptions.setPage(pageNumber); + + doc.addSignature(signature, instance, signatureOptions); + + } else { + doc.addSignature(signature, instance); + } + doc.saveIncremental(output); + } catch (Exception e) { + log.error("exception", e); + } + } + + @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") + @Operation( + summary = "Sign PDF with a Digital Certificate", + description = + "This endpoint accepts a PDF file, a digital certificate and related information to sign" + + " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF" + + " Type:SISO") + public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) + throws Exception { + MultipartFile pdf = request.getFileInput(); + String certType = request.getCertType(); + MultipartFile privateKeyFile = request.getPrivateKeyFile(); + MultipartFile certFile = request.getCertFile(); + MultipartFile p12File = request.getP12File(); + MultipartFile jksfile = request.getJksFile(); + String password = request.getPassword(); + Boolean showSignature = request.isShowSignature(); + String reason = request.getReason(); + String location = request.getLocation(); + String name = request.getName(); + Integer pageNumber = request.getPageNumber() - 1; + Boolean showLogo = request.isShowLogo(); + + if (certType == null) { + throw new IllegalArgumentException("Cert type must be provided"); + } + + KeyStore ks = null; + + switch (certType) { + case "PEM": + ks = KeyStore.getInstance("JKS"); + ks.load(null); + PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password); + Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes()); + ks.setKeyEntry( + "alias", privateKey, password.toCharArray(), new Certificate[] {cert}); + break; + case "PKCS12": + ks = KeyStore.getInstance("PKCS12"); + ks.load(p12File.getInputStream(), password.toCharArray()); + break; + case "JKS": + ks = KeyStore.getInstance("JKS"); + ks.load(jksfile.getInputStream(), password.toCharArray()); + break; + default: + throw new IllegalArgumentException("Invalid cert type: " + certType); + } + + CreateSignature createSignature = new CreateSignature(ks, password.toCharArray()); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + sign( + pdfDocumentFactory, + pdf.getBytes(), + baos, + createSignature, + showSignature, + pageNumber, + name, + location, + reason, + showLogo); + return WebResponseUtils.boasToWebResponse( + baos, + Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") + + "_signed.pdf"); + } + + private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password) + throws IOException, OperatorCreationException, PKCSException { + try (PEMParser pemParser = + new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) { + Object pemObject = pemParser.readObject(); + JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); + PrivateKeyInfo pkInfo; + if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) { + InputDecryptorProvider decProv = + new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); + pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv); + } else if (pemObject instanceof PEMEncryptedKeyPair) { + PEMDecryptorProvider decProv = + new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); + pkInfo = + ((PEMEncryptedKeyPair) pemObject) + .decryptKeyPair(decProv) + .getPrivateKeyInfo(); + } else { + pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo(); + } + return converter.getPrivateKey(pkInfo); + } + } + + private Certificate getCertificateFromPEM(byte[] pemBytes) + throws IOException, CertificateException { + try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) { + return CertificateFactory.getInstance("X.509").generateCertificate(bis); + } + } + class CreateSignature extends CreateSignatureBase { File logoFile; @@ -198,149 +332,4 @@ public class CertSignController { } } } - - private final CustomPDDocumentFactory pdfDocumentFactory; - - @Autowired - public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) { - this.pdfDocumentFactory = pdfDocumentFactory; - } - - @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") - @Operation( - summary = "Sign PDF with a Digital Certificate", - description = - "This endpoint accepts a PDF file, a digital certificate and related information to sign" - + " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF" - + " Type:SISO") - public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) - throws Exception { - MultipartFile pdf = request.getFileInput(); - String certType = request.getCertType(); - MultipartFile privateKeyFile = request.getPrivateKeyFile(); - MultipartFile certFile = request.getCertFile(); - MultipartFile p12File = request.getP12File(); - MultipartFile jksfile = request.getJksFile(); - String password = request.getPassword(); - Boolean showSignature = request.isShowSignature(); - String reason = request.getReason(); - String location = request.getLocation(); - String name = request.getName(); - Integer pageNumber = request.getPageNumber() - 1; - Boolean showLogo = request.isShowLogo(); - - if (certType == null) { - throw new IllegalArgumentException("Cert type must be provided"); - } - - KeyStore ks = null; - - switch (certType) { - case "PEM": - ks = KeyStore.getInstance("JKS"); - ks.load(null); - PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password); - Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes()); - ks.setKeyEntry( - "alias", privateKey, password.toCharArray(), new Certificate[] {cert}); - break; - case "PKCS12": - ks = KeyStore.getInstance("PKCS12"); - ks.load(p12File.getInputStream(), password.toCharArray()); - break; - case "JKS": - ks = KeyStore.getInstance("JKS"); - ks.load(jksfile.getInputStream(), password.toCharArray()); - break; - default: - throw new IllegalArgumentException("Invalid cert type: " + certType); - } - - CreateSignature createSignature = new CreateSignature(ks, password.toCharArray()); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - sign( - pdfDocumentFactory, - pdf.getBytes(), - baos, - createSignature, - showSignature, - pageNumber, - name, - location, - reason, - showLogo); - return WebResponseUtils.boasToWebResponse( - baos, - Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "") - + "_signed.pdf"); - } - - private static void sign( - CustomPDDocumentFactory pdfDocumentFactory, - byte[] input, - OutputStream output, - CreateSignature instance, - Boolean showSignature, - Integer pageNumber, - String name, - String location, - String reason, - Boolean showLogo) { - try (PDDocument doc = pdfDocumentFactory.load(input)) { - PDSignature signature = new PDSignature(); - signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); - signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); - signature.setName(name); - signature.setLocation(location); - signature.setReason(reason); - signature.setSignDate(Calendar.getInstance()); - - if (showSignature) { - SignatureOptions signatureOptions = new SignatureOptions(); - signatureOptions.setVisualSignature( - instance.createVisibleSignature(doc, signature, pageNumber, showLogo)); - signatureOptions.setPage(pageNumber); - - doc.addSignature(signature, instance, signatureOptions); - - } else { - doc.addSignature(signature, instance); - } - doc.saveIncremental(output); - } catch (Exception e) { - log.error("exception", e); - } - } - - private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password) - throws IOException, OperatorCreationException, PKCSException { - try (PEMParser pemParser = - new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) { - Object pemObject = pemParser.readObject(); - JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC"); - PrivateKeyInfo pkInfo; - if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) { - InputDecryptorProvider decProv = - new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray()); - pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv); - } else if (pemObject instanceof PEMEncryptedKeyPair) { - PEMDecryptorProvider decProv = - new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); - pkInfo = - ((PEMEncryptedKeyPair) pemObject) - .decryptKeyPair(decProv) - .getPrivateKeyInfo(); - } else { - pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo(); - } - return converter.getPrivateKey(pkInfo); - } - } - - private Certificate getCertificateFromPEM(byte[] pemBytes) - throws IOException, CertificateException { - try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) { - return CertificateFactory.getInstance("X.509").generateCertificate(bis); - } - } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 5e5349342..fc9e86231 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -4,25 +4,13 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import org.apache.pdfbox.Loader; import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSString; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentCatalog; -import org.apache.pdfbox.pdmodel.PDDocumentInformation; -import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; -import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; -import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDStream; @@ -83,6 +71,48 @@ public class GetInfoOnPDF { static ObjectMapper objectMapper = new ObjectMapper(); + private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { + if (outline == null) return; + + ObjectNode outlineNode = objectMapper.createObjectNode(); + outlineNode.put("Title", outline.getTitle()); + // You can add other properties if needed + arrayNode.add(outlineNode); + + PDOutlineItem child = outline.getFirstChild(); + while (child != null) { + addOutlinesToArray(child, arrayNode); + child = child.getNextSibling(); + } + } + + public static boolean checkForStandard(PDDocument document, String standardKeyword) { + // Check XMP Metadata + try { + PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); + if (pdMetadata != null) { + COSInputStream metaStream = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(metaStream); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, baos, true); + String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); + + if (xmpString.contains(standardKeyword)) { + return true; + } + } + } catch ( + Exception + e) { // Catching general exception for brevity, ideally you'd catch specific + // exceptions. + log.error("exception", e); + } + + return false; + } + @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { @@ -606,21 +636,6 @@ public class GetInfoOnPDF { return state ? "Allowed" : "Not Allowed"; } - private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { - if (outline == null) return; - - ObjectNode outlineNode = objectMapper.createObjectNode(); - outlineNode.put("Title", outline.getTitle()); - // You can add other properties if needed - arrayNode.add(outlineNode); - - PDOutlineItem child = outline.getFirstChild(); - while (child != null) { - addOutlinesToArray(child, arrayNode); - child = child.getNextSibling(); - } - } - public String getPageOrientation(double width, double height) { if (width > height) { return "Landscape"; @@ -678,33 +693,6 @@ public class GetInfoOnPDF { return dimensionInfo; } - public static boolean checkForStandard(PDDocument document, String standardKeyword) { - // Check XMP Metadata - try { - PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); - if (pdMetadata != null) { - COSInputStream metaStream = pdMetadata.createInputStream(); - DomXmpParser domXmpParser = new DomXmpParser(); - XMPMetadata xmpMeta = domXmpParser.parse(metaStream); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new XmpSerializer().serialize(xmpMeta, baos, true); - String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); - - if (xmpString.contains(standardKeyword)) { - return true; - } - } - } catch ( - Exception - e) { // Catching general exception for brevity, ideally you'd catch specific - // exceptions. - log.error("exception", e); - } - - return false; - } - public ArrayNode exploreStructureTree(List nodes) { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { 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 aab4aeccc..b52204544 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 @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api.security; -import java.awt.Color; +import java.awt.*; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index b034d45d6..bd8904fa2 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -4,17 +4,9 @@ import java.io.IOException; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentCatalog; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageTree; -import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.common.PDMetadata; -import org.apache.pdfbox.pdmodel.interactive.action.PDAction; -import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; -import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch; -import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; -import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions; +import org.apache.pdfbox.pdmodel.interactive.action.*; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java b/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java index 317c6424b..13be9a39c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/ValidateSignatureController.java @@ -14,11 +14,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cms.CMSProcessable; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.SignerInformation; -import org.bouncycastle.cms.SignerInformationStore; +import org.bouncycastle.cms.*; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.util.Store; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index bb74e9f1d..6bf0f96b4 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -1,6 +1,6 @@ package stirling.software.SPDF.controller.api.security; -import java.awt.Color; +import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index 3e478af25..81b46166b 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -5,7 +5,6 @@ import java.time.temporal.ChronoUnit; import java.util.*; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -38,24 +37,30 @@ import stirling.software.SPDF.repository.UserRepository; @Tag(name = "Account Security", description = "Account Security APIs") public class AccountWebController { - @Autowired ApplicationProperties applicationProperties; - @Autowired SessionPersistentRegistry sessionPersistentRegistry; + private final ApplicationProperties applicationProperties; - @Autowired - private UserRepository userRepository; // Assuming you have a repository for user operations + private final SessionPersistentRegistry sessionPersistentRegistry; + + private final UserRepository // Assuming you have a repository for user operations + userRepository; + + public AccountWebController( + ApplicationProperties applicationProperties, + SessionPersistentRegistry sessionPersistentRegistry, + UserRepository userRepository) { + this.applicationProperties = applicationProperties; + this.sessionPersistentRegistry = sessionPersistentRegistry; + this.userRepository = userRepository; + } @GetMapping("/login") public String login(HttpServletRequest request, Model model, Authentication authentication) { - // If the user is already authenticated, redirect them to the home page. if (authentication != null && authentication.isAuthenticated()) { return "redirect:/"; } - Map providerList = new HashMap<>(); - Security securityProps = applicationProperties.getSecurity(); - OAUTH2 oauth = securityProps.getOauth2(); if (oauth != null) { if (oauth.getEnabled()) { @@ -70,14 +75,12 @@ public class AccountWebController { "/oauth2/authorization/" + google.getName(), google.getClientName()); } - GithubProvider github = client.getGithub(); if (github.isSettingsValid()) { providerList.put( "/oauth2/authorization/" + github.getName(), github.getClientName()); } - KeycloakProvider keycloak = client.getKeycloak(); if (keycloak.isSettingsValid()) { providerList.put( @@ -87,7 +90,6 @@ public class AccountWebController { } } } - SAML2 saml2 = securityProps.getSaml2(); if (securityProps.isSaml2Activ() && applicationProperties.getSystem().getEnableAlphaFunctionality()) { @@ -98,16 +100,12 @@ public class AccountWebController { .entrySet() .removeIf(entry -> entry.getKey() == null || entry.getValue() == null); model.addAttribute("providerlist", providerList); - model.addAttribute("loginMethod", securityProps.getLoginMethod()); boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false; model.addAttribute("altLogin", altLogin); - model.addAttribute("currentPage", "login"); - String error = request.getParameter("error"); if (error != null) { - switch (error) { case "badcredentials": error = "login.invalid"; @@ -121,12 +119,10 @@ public class AccountWebController { default: break; } - model.addAttribute("error", error); } String erroroauth = request.getParameter("erroroauth"); if (erroroauth != null) { - switch (erroroauth) { case "oauth2AutoCreateDisabled": erroroauth = "login.oauth2AutoCreateDisabled"; @@ -178,18 +174,14 @@ public class AccountWebController { default: break; } - model.addAttribute("erroroauth", erroroauth); } if (request.getParameter("messageType") != null) { - model.addAttribute("messageType", "changedCredsMessage"); } if (request.getParameter("logout") != null) { - model.addAttribute("logoutMessage", "You have been logged out."); } - return "login"; } @@ -200,14 +192,11 @@ public class AccountWebController { List allUsers = userRepository.findAll(); Iterator iterator = allUsers.iterator(); Map roleDetails = Role.getAllRoleDetails(); - // Map to store session information and user activity status Map userSessions = new HashMap<>(); Map userLastRequest = new HashMap<>(); - int activeUsers = 0; int disabledUsers = 0; - while (iterator.hasNext()) { User user = iterator.next(); if (user != null) { @@ -215,22 +204,20 @@ public class AccountWebController { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { iterator.remove(); roleDetails.remove(Role.INTERNAL_API_USER.getRoleId()); - break; // Break out of the inner loop once the user is removed + // Break out of the inner loop once the user is removed + break; } } - // Determine the user's session status and last request time int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); boolean hasActiveSession = false; Date lastRequest = null; - Optional latestSession = sessionPersistentRegistry.findLatestSession(user.getUsername()); if (latestSession.isPresent()) { SessionEntity sessionEntity = latestSession.get(); Date lastAccessedTime = sessionEntity.getLastRequest(); Instant now = Instant.now(); - // Calculate session expiration and update session status accordingly Instant expirationTime = lastAccessedTime @@ -242,16 +229,14 @@ public class AccountWebController { } else { hasActiveSession = !sessionEntity.isExpired(); } - lastRequest = sessionEntity.getLastRequest(); } else { hasActiveSession = false; - lastRequest = new Date(0); // No session, set default last request time + // No session, set default last request time + lastRequest = new Date(0); } - userSessions.put(user.getUsername(), hasActiveSession); userLastRequest.put(user.getUsername(), lastRequest); - if (hasActiveSession) { activeUsers++; } @@ -260,7 +245,6 @@ public class AccountWebController { } } } - // Sort users by active status and last request date List sortedUsers = allUsers.stream() @@ -268,7 +252,6 @@ public class AccountWebController { (u1, u2) -> { boolean u1Active = userSessions.get(u1.getUsername()); boolean u2Active = userSessions.get(u2.getUsername()); - if (u1Active && !u2Active) { return -1; } else if (!u1Active && u2Active) { @@ -284,9 +267,7 @@ public class AccountWebController { } }) .collect(Collectors.toList()); - String messageType = request.getParameter("messageType"); - String deleteMessage = null; if (messageType != null) { switch (messageType) { @@ -300,7 +281,6 @@ public class AccountWebController { break; } model.addAttribute("deleteMessage", deleteMessage); - String addMessage = null; switch (messageType) { case "usernameExists": @@ -317,7 +297,6 @@ public class AccountWebController { } model.addAttribute("addMessage", addMessage); } - String changeMessage = null; if (messageType != null) { switch (messageType) { @@ -336,7 +315,6 @@ public class AccountWebController { } model.addAttribute("changeMessage", changeMessage); } - model.addAttribute("users", sortedUsers); model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("roleDetails", roleDetails); @@ -357,21 +335,17 @@ public class AccountWebController { if (authentication != null && authentication.isAuthenticated()) { Object principal = authentication.getPrincipal(); String username = null; - if (principal instanceof UserDetails) { // Cast the principal object to UserDetails UserDetails userDetails = (UserDetails) principal; - // Retrieve username and other attributes username = userDetails.getUsername(); - // Add oAuth2 Login attributes to the model model.addAttribute("oAuth2Login", false); } if (principal instanceof OAuth2User) { // Cast the principal object to OAuth2User OAuth2User userDetails = (OAuth2User) principal; - // Retrieve username and other attributes username = userDetails.getAttribute( @@ -383,22 +357,21 @@ public class AccountWebController { // Cast the principal object to OAuth2User CustomSaml2AuthenticatedPrincipal userDetails = (CustomSaml2AuthenticatedPrincipal) principal; - // Retrieve username and other attributes username = userDetails.getName(); // Add oAuth2 Login attributes to the model model.addAttribute("oAuth2Login", true); } - if (username != null) { // Fetch user details from the database Optional user = - userRepository.findByUsernameIgnoreCaseWithSettings( - username); // Assuming findByUsername method exists + userRepository + .findByUsernameIgnoreCaseWithSettings( // Assuming findByUsername + // method exists + username); if (!user.isPresent()) { return "redirect:/error"; } - // Convert settings map to JSON string ObjectMapper objectMapper = new ObjectMapper(); String settingsJson; @@ -409,7 +382,6 @@ public class AccountWebController { log.error("exception", e); return "redirect:/error"; } - String messageType = request.getParameter("messageType"); if (messageType != null) { switch (messageType) { @@ -433,7 +405,6 @@ public class AccountWebController { } model.addAttribute("messageType", messageType); } - // Add attributes to the model model.addAttribute("username", username); model.addAttribute("role", user.get().getRolesAsString()); @@ -456,23 +427,21 @@ public class AccountWebController { } if (authentication != null && authentication.isAuthenticated()) { Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails) { // Cast the principal object to UserDetails UserDetails userDetails = (UserDetails) principal; - // Retrieve username and other attributes String username = userDetails.getUsername(); - // Fetch user details from the database Optional user = - userRepository.findByUsernameIgnoreCase( - username); // Assuming findByUsername method exists + userRepository + .findByUsernameIgnoreCase( // Assuming findByUsername method exists + username); if (!user.isPresent()) { // Handle error appropriately - return "redirect:/error"; // Example redirection in case of error + // Example redirection in case of error + return "redirect:/error"; } - String messageType = request.getParameter("messageType"); if (messageType != null) { switch (messageType) { @@ -493,7 +462,6 @@ public class AccountWebController { } model.addAttribute("messageType", messageType); } - // Add attributes to the model model.addAttribute("username", username); } diff --git a/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java index 5f521b50e..eafcbd267 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.web; import java.util.List; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; @@ -19,25 +18,25 @@ import stirling.software.SPDF.utils.FileInfo; @Tag(name = "Database Management", description = "Database management and security APIs") public class DatabaseWebController { - @Autowired private DatabaseBackupHelper databaseBackupHelper; + private final DatabaseBackupHelper databaseBackupHelper; + + public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) { + this.databaseBackupHelper = databaseBackupHelper; + } @PreAuthorize("hasRole('ROLE_ADMIN')") @GetMapping("/database") public String database(HttpServletRequest request, Model model, Authentication authentication) { String error = request.getParameter("error"); String confirmed = request.getParameter("infoMessage"); - if (error != null) { model.addAttribute("error", error); } else if (confirmed != null) { model.addAttribute("infoMessage", confirmed); } - List backupList = databaseBackupHelper.getBackupList(); model.addAttribute("backupFiles", backupList); - model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version()); - return "database"; } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 7845fc29b..0043c5e1d 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -6,12 +6,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -39,31 +34,41 @@ import stirling.software.SPDF.service.SignatureService; @Slf4j public class GeneralWebController { + private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/"; + private static final String ALL_USERS_FOLDER = "ALL_USERS"; + private final SignatureService signatureService; + private final UserServiceInterface userService; + private final ResourceLoader resourceLoader; + + public GeneralWebController( + SignatureService signatureService, + @Autowired(required = false) UserServiceInterface userService, + ResourceLoader resourceLoader) { + this.signatureService = signatureService; + this.userService = userService; + this.resourceLoader = resourceLoader; + } + @GetMapping("/pipeline") @Hidden public String pipelineForm(Model model) { model.addAttribute("currentPage", "pipeline"); - List pipelineConfigs = new ArrayList<>(); List> pipelineConfigsWithNames = new ArrayList<>(); - if (new File("./pipeline/defaultWebUIConfigs/").exists()) { try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { List jsonFiles = paths.filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".json")) .collect(Collectors.toList()); - for (Path jsonFile : jsonFiles) { String content = Files.readString(jsonFile, StandardCharsets.UTF_8); pipelineConfigs.add(content); } - for (String config : pipelineConfigs) { Map jsonContent = new ObjectMapper() .readValue(config, new TypeReference>() {}); - String name = (String) jsonContent.get("name"); if (name == null || name.length() < 1) { String filename = @@ -78,7 +83,6 @@ public class GeneralWebController { configWithName.put("name", name); pipelineConfigsWithNames.add(configWithName); } - } catch (IOException e) { log.error("exception", e); } @@ -90,9 +94,7 @@ public class GeneralWebController { pipelineConfigsWithNames.add(configWithName); } model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); - model.addAttribute("pipelineConfigs", pipelineConfigs); - return "pipeline"; } @@ -173,14 +175,6 @@ public class GeneralWebController { return "split-pdfs"; } - private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/"; - private static final String ALL_USERS_FOLDER = "ALL_USERS"; - - @Autowired private SignatureService signatureService; - - @Autowired(required = false) - private UserServiceInterface userService; - @GetMapping("/sign") @Hidden public String signForm(Model model) { @@ -188,10 +182,8 @@ public class GeneralWebController { if (userService != null) { username = userService.getCurrentUsername(); } - // Get signatures from both personal and ALL_USERS folders List signatures = signatureService.getAvailableSignatures(username); - model.addAttribute("currentPage", "sign"); model.addAttribute("fonts", getFontNames()); model.addAttribute("signatures", signatures); @@ -226,17 +218,12 @@ public class GeneralWebController { return "overlay-pdf"; } - @Autowired private ResourceLoader resourceLoader; - private List getFontNames() { List fontNames = new ArrayList<>(); - // Extract font names from classpath fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2")); - // Extract font names from external directory fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*")); - return fontNames; } @@ -283,13 +270,38 @@ public class GeneralWebController { case "svg": return "svg"; default: - return ""; // or throw an exception if an unexpected extension is encountered + // or throw an exception if an unexpected extension is encountered + return ""; } } + @GetMapping("/crop") + @Hidden + public String cropForm(Model model) { + model.addAttribute("currentPage", "crop"); + return "crop"; + } + + @GetMapping("/auto-split-pdf") + @Hidden + public String autoSPlitPDFForm(Model model) { + model.addAttribute("currentPage", "auto-split-pdf"); + return "auto-split-pdf"; + } + + @GetMapping("/remove-image-pdf") + @Hidden + public String removeImagePdfForm(Model model) { + model.addAttribute("currentPage", "remove-image-pdf"); + return "remove-image-pdf"; + } + public class FontResource { + private String name; + private String extension; + private String type; public FontResource(String name, String extension) { @@ -322,25 +334,4 @@ public class GeneralWebController { this.type = type; } } - - @GetMapping("/crop") - @Hidden - public String cropForm(Model model) { - model.addAttribute("currentPage", "crop"); - return "crop"; - } - - @GetMapping("/auto-split-pdf") - @Hidden - public String autoSPlitPDFForm(Model model) { - model.addAttribute("currentPage", "auto-split-pdf"); - return "auto-split-pdf"; - } - - @GetMapping("/remove-image-pdf") - @Hidden - public String removeImagePdfForm(Model model) { - model.addAttribute("currentPage", "remove-image-pdf"); - return "remove-image-pdf"; - } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java index dc67be742..ddc99a82b 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -6,7 +6,6 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; @@ -28,6 +27,12 @@ import stirling.software.SPDF.model.Dependency; @Slf4j public class HomeWebController { + private final ApplicationProperties applicationProperties; + + public HomeWebController(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } + @GetMapping("/about") @Hidden public String gameForm(Model model) { @@ -69,8 +74,6 @@ public class HomeWebController { return "redirect:/"; } - @Autowired ApplicationProperties applicationProperties; - @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody @Hidden diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index ff73fb2d3..6479c7afe 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -5,7 +5,6 @@ import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -30,12 +29,18 @@ import stirling.software.SPDF.model.ApplicationProperties; @Slf4j public class MetricsController { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; private final MeterRegistry meterRegistry; private boolean metricsEnabled; + public MetricsController( + ApplicationProperties applicationProperties, MeterRegistry meterRegistry) { + this.applicationProperties = applicationProperties; + this.meterRegistry = meterRegistry; + } + @PostConstruct public void init() { Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); @@ -43,11 +48,6 @@ public class MetricsController { this.metricsEnabled = metricsEnabled; } - @Autowired - public MetricsController(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } - @GetMapping("/status") @Operation( summary = "Application status and version", @@ -57,7 +57,6 @@ public class MetricsController { if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } - Map status = new HashMap<>(); status.put("status", "UP"); status.put("version", getClass().getPackage().getImplementationVersion()); @@ -236,7 +235,6 @@ public class MetricsController { String uri = counter.getId().getTag("uri"); counts.merge(uri, counter.count(), Double::sum); }); - List result = counts.entrySet().stream() .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) @@ -271,7 +269,6 @@ public class MetricsController { private List getUniqueUserCounts(String method) { log.info("Getting unique user counts for method: {}", method); Map> uniqueUsers = new HashMap<>(); - meterRegistry .find("http.requests") .tag("method", method) @@ -284,19 +281,37 @@ public class MetricsController { uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session); } }); - List result = uniqueUsers.entrySet().stream() .map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size())) .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .collect(Collectors.toList()); - log.info("Found {} endpoints with unique user counts", result.size()); return result; } + @GetMapping("/uptime") + public ResponseEntity getUptime() { + if (!metricsEnabled) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); + } + LocalDateTime now = LocalDateTime.now(); + Duration uptime = Duration.between(StartupApplicationListener.startTime, now); + return ResponseEntity.ok(formatDuration(uptime)); + } + + private String formatDuration(Duration duration) { + long days = duration.toDays(); + long hours = duration.toHoursPart(); + long minutes = duration.toMinutesPart(); + long seconds = duration.toSecondsPart(); + return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds); + } + public static class EndpointCount { + private String endpoint; + private double count; public EndpointCount(String endpoint, double count) { @@ -320,23 +335,4 @@ public class MetricsController { this.count = count; } } - - @GetMapping("/uptime") - public ResponseEntity getUptime() { - if (!metricsEnabled) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); - } - - LocalDateTime now = LocalDateTime.now(); - Duration uptime = Duration.between(StartupApplicationListener.startTime, now); - return ResponseEntity.ok(formatDuration(uptime)); - } - - private String formatDuration(Duration duration) { - long days = duration.toDays(); - long hours = duration.toHoursPart(); - long minutes = duration.toMinutesPart(); - long seconds = duration.toSecondsPart(); - return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds); - } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index 7f87d4f22..0732a2270 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -6,7 +6,6 @@ import java.util.Collections; import java.util.List; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -22,7 +21,11 @@ import stirling.software.SPDF.utils.CheckProgramInstall; @Tag(name = "Misc", description = "Miscellaneous APIs") public class OtherWebController { - @Autowired ApplicationProperties applicationProperties; + private final ApplicationProperties applicationProperties; + + public OtherWebController(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + } @GetMapping("/compress-pdf") @Hidden diff --git a/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java b/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java index eb375663c..eaf671393 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SignatureController.java @@ -18,10 +18,16 @@ import stirling.software.SPDF.service.SignatureService; @RequestMapping("/api/v1/general") public class SignatureController { - @Autowired private SignatureService signatureService; + private final SignatureService signatureService; - @Autowired(required = false) - private UserServiceInterface userService; + private final UserServiceInterface userService; + + public SignatureController( + SignatureService signatureService, + @Autowired(required = false) UserServiceInterface userService) { + this.signatureService = signatureService; + this.userService = userService; + } @GetMapping("/sign/{fileName}") public ResponseEntity getSignature(@PathVariable(name = "fileName") String fileName) @@ -30,15 +36,14 @@ public class SignatureController { if (userService != null) { username = userService.getCurrentUsername(); } - // Verify access permission if (!signatureService.hasAccessToFile(username, fileName)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); } - byte[] imageBytes = signatureService.getSignatureBytes(username, fileName); return ResponseEntity.ok() - .contentType(MediaType.IMAGE_JPEG) // Adjust based on file type + .contentType( // Adjust based on file type + MediaType.IMAGE_JPEG) .body(imageBytes); } } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index fd7c278bf..3b2d61762 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -79,6 +79,23 @@ public class ApplicationProperties { return saml2.getEnabled() || oauth2.getEnabled(); } + public boolean isUserPass() { + return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()) + || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); + } + + public boolean isOauth2Activ() { + return (oauth2 != null + && oauth2.getEnabled() + && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); + } + + public boolean isSaml2Activ() { + return (saml2 != null + && saml2.getEnabled() + && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); + } + public enum LoginMethods { ALL("all"), NORMAL("normal"), @@ -97,23 +114,6 @@ public class ApplicationProperties { } } - public boolean isUserPass() { - return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()) - || loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString())); - } - - public boolean isOauth2Activ() { - return (oauth2 != null - && oauth2.getEnabled() - && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); - } - - public boolean isSaml2Activ() { - return (saml2 != null - && saml2.getEnabled() - && !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())); - } - @Data public static class InitialLogin { private String username; diff --git a/src/main/java/stirling/software/SPDF/model/Authority.java b/src/main/java/stirling/software/SPDF/model/Authority.java index 8dd6d6e74..be250e8b8 100644 --- a/src/main/java/stirling/software/SPDF/model/Authority.java +++ b/src/main/java/stirling/software/SPDF/model/Authority.java @@ -2,14 +2,7 @@ package stirling.software.SPDF.model; import java.io.Serializable; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; +import jakarta.persistence.*; @Entity @Table(name = "authorities") @@ -17,14 +10,6 @@ public class Authority implements Serializable { private static final long serialVersionUID = 1L; - public Authority() {} - - public Authority(String authority, User user) { - this.authority = authority; - this.user = user; - user.getAuthorities().add(this); - } - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -36,6 +21,14 @@ public class Authority implements Serializable { @JoinColumn(name = "user_id") private User user; + public Authority() {} + + public Authority(String authority, User user) { + this.authority = authority; + this.user = user; + user.getAuthorities().add(this); + } + public Long getId() { return id; } diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java index 02e7dd5f1..78f93d867 100644 --- a/src/main/java/stirling/software/SPDF/model/Role.java +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -40,22 +40,6 @@ public enum Role { this.roleName = roleName; } - public String getRoleId() { - return roleId; - } - - public int getApiCallsPerDay() { - return apiCallsPerDay; - } - - public int getWebCallsPerDay() { - return webCallsPerDay; - } - - public String getRoleName() { - return roleName; - } - public static String getRoleNameByRoleId(String roleId) { // Using the fromString method to get the Role enum based on the roleId Role role = fromString(roleId); @@ -81,4 +65,20 @@ public enum Role { } throw new IllegalArgumentException("No Role defined for id: " + roleId); } + + public String getRoleId() { + return roleId; + } + + public int getApiCallsPerDay() { + return apiCallsPerDay; + } + + public int getWebCallsPerDay() { + return webCallsPerDay; + } + + public String getRoleName() { + return roleName; + } } diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index ddfb71353..56bcdf332 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -111,14 +111,14 @@ public class User implements Serializable { this.enabled = enabled; } - public void setAuthenticationType(AuthenticationType authenticationType) { - this.authenticationType = authenticationType.toString().toLowerCase(); - } - public String getAuthenticationType() { return authenticationType; } + public void setAuthenticationType(AuthenticationType authenticationType) { + this.authenticationType = authenticationType.toString().toLowerCase(); + } + public Set getAuthorities() { return authorities; } diff --git a/src/main/java/stirling/software/SPDF/model/provider/GithubProvider.java b/src/main/java/stirling/software/SPDF/model/provider/GithubProvider.java index 85fe72585..afe7fcb77 100644 --- a/src/main/java/stirling/software/SPDF/model/provider/GithubProvider.java +++ b/src/main/java/stirling/software/SPDF/model/provider/GithubProvider.java @@ -12,6 +12,10 @@ public class GithubProvider extends Provider { private static final String authorizationUri = "https://github.com/login/oauth/authorize"; private static final String tokenUri = "https://github.com/login/oauth/access_token"; private static final String userInfoUri = "https://api.github.com/user"; + private String clientId; + private String clientSecret; + private Collection scopes = new ArrayList<>(); + private String useAsUsername = "login"; public String getAuthorizationuri() { return authorizationUri; @@ -25,11 +29,6 @@ public class GithubProvider extends Provider { return userInfoUri; } - private String clientId; - private String clientSecret; - private Collection scopes = new ArrayList<>(); - private String useAsUsername = "login"; - @Override public String getIssuer() { return new String(); diff --git a/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java b/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java index a3608df8c..e43e1327b 100644 --- a/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java +++ b/src/main/java/stirling/software/SPDF/model/provider/GoogleProvider.java @@ -13,6 +13,10 @@ public class GoogleProvider extends Provider { private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token"; private static final String userInfoUri = "https://www.googleapis.com/oauth2/v3/userinfo?alt=json"; + private String clientId; + private String clientSecret; + private Collection scopes = new ArrayList<>(); + private String useAsUsername = "email"; public String getAuthorizationuri() { return authorizationUri; @@ -26,11 +30,6 @@ public class GoogleProvider extends Provider { return userInfoUri; } - private String clientId; - private String clientSecret; - private Collection scopes = new ArrayList<>(); - private String useAsUsername = "email"; - @Override public String getIssuer() { return new String(); diff --git a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index 77f7d0c6f..484f65241 100644 --- a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -21,16 +21,6 @@ public class TextFinder extends PDFTextStripper { private final boolean wholeWordSearch; private final List textOccurrences = new ArrayList<>(); - private class MatchInfo { - int startIndex; - int matchLength; - - MatchInfo(int startIndex, int matchLength) { - this.startIndex = startIndex; - this.matchLength = matchLength; - } - } - public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) throws IOException { this.searchText = searchText.toLowerCase(); @@ -103,4 +93,14 @@ public class TextFinder extends PDFTextStripper { return textOccurrences; } + + private class MatchInfo { + int startIndex; + int matchLength; + + MatchInfo(int startIndex, int matchLength) { + this.startIndex = startIndex; + this.matchLength = matchLength; + } + } } diff --git a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java index 7b2e58ffc..98becfd48 100644 --- a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java +++ b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java @@ -2,7 +2,6 @@ package stirling.software.SPDF.repository; import java.util.Date; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.transaction.annotation.Transactional; @@ -11,7 +10,11 @@ import stirling.software.SPDF.model.PersistentLogin; public class JPATokenRepositoryImpl implements PersistentTokenRepository { - @Autowired private PersistentLoginRepository persistentLoginRepository; + private final PersistentLoginRepository persistentLoginRepository; + + public JPATokenRepositoryImpl(PersistentLoginRepository persistentLoginRepository) { + this.persistentLoginRepository = persistentLoginRepository; + } @Override @Transactional diff --git a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java index 550db6801..7b4dc6dd8 100644 --- a/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java +++ b/src/main/java/stirling/software/SPDF/service/CertificateValidationService.java @@ -1,25 +1,10 @@ package stirling.software.SPDF.service; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.security.KeyStore; import java.security.KeyStoreException; -import java.security.cert.CertPath; -import java.security.cert.CertPathValidator; -import java.security.cert.CertificateExpiredException; -import java.security.cert.CertificateFactory; -import java.security.cert.CertificateNotYetValidException; -import java.security.cert.PKIXParameters; -import java.security.cert.TrustAnchor; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.security.cert.*; +import java.util.*; import org.springframework.stereotype.Service; diff --git a/src/main/java/stirling/software/SPDF/service/PostHogService.java b/src/main/java/stirling/software/SPDF/service/PostHogService.java index 3127e2af5..f47693f9d 100644 --- a/src/main/java/stirling/software/SPDF/service/PostHogService.java +++ b/src/main/java/stirling/software/SPDF/service/PostHogService.java @@ -6,11 +6,7 @@ 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 java.util.*; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/stirling/software/SPDF/utils/FileInfo.java b/src/main/java/stirling/software/SPDF/utils/FileInfo.java index 4e236756a..64e21f8c1 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileInfo.java +++ b/src/main/java/stirling/software/SPDF/utils/FileInfo.java @@ -11,15 +11,14 @@ import lombok.Data; @AllArgsConstructor @Data public class FileInfo { + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private String fileName; private String filePath; private LocalDateTime modificationDate; private long fileSize; private LocalDateTime creationDate; - private static final DateTimeFormatter DATE_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - // Converts the file path string to a Path object. public Path getFilePathAsPath() { return Paths.get(filePath); diff --git a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java index ecfe3e5d9..442167d8c 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java +++ b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java @@ -1,11 +1,6 @@ package stirling.software.SPDF.utils; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.UncheckedIOException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 73f5167e1..ac4cdca8f 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -4,18 +4,9 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.NetworkInterface; -import java.net.URI; -import java.net.URL; +import java.net.*; import java.nio.charset.StandardCharsets; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; +import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.MessageDigest; import java.util.ArrayList; diff --git a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java index 24cb72ebe..8bec891cc 100644 --- a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java @@ -1,11 +1,7 @@ package stirling.software.SPDF.utils; import java.awt.geom.AffineTransform; -import java.awt.image.AffineTransformOp; -import java.awt.image.BufferedImage; -import java.awt.image.DataBuffer; -import java.awt.image.DataBufferByte; -import java.awt.image.DataBufferInt; +import java.awt.image.*; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 55a83cf2e..78b773efd 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -126,82 +126,6 @@ public class PdfUtils { return pageText.contains(phrase); } - public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) - throws IOException { - PDFTextStripper textStripper = new PDFTextStripper(); - String pdfText = ""; - - if (pagesToCheck == null || "all".equals(pagesToCheck)) { - pdfText = textStripper.getText(pdfDocument); - } else { - // remove whitespaces - pagesToCheck = pagesToCheck.replaceAll("\\s+", ""); - - String[] splitPoints = pagesToCheck.split(","); - for (String splitPoint : splitPoints) { - if (splitPoint.contains("-")) { - // Handle page ranges - String[] range = splitPoint.split("-"); - int startPage = Integer.parseInt(range[0]); - int endPage = Integer.parseInt(range[1]); - - for (int i = startPage; i <= endPage; i++) { - textStripper.setStartPage(i); - textStripper.setEndPage(i); - pdfText += textStripper.getText(pdfDocument); - } - } else { - // Handle individual page - int page = Integer.parseInt(splitPoint); - textStripper.setStartPage(page); - textStripper.setEndPage(page); - pdfText += textStripper.getText(pdfDocument); - } - } - } - - pdfDocument.close(); - - return pdfText.contains(text); - } - - public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) - throws IOException { - int actualPageCount = pdfDocument.getNumberOfPages(); - pdfDocument.close(); - - switch (comparator.toLowerCase()) { - case "greater": - return actualPageCount > pageCount; - case "equal": - return actualPageCount == pageCount; - case "less": - return actualPageCount < pageCount; - default: - throw new IllegalArgumentException( - "Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); - } - } - - public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException { - PDPage firstPage = pdfDocument.getPage(0); - PDRectangle mediaBox = firstPage.getMediaBox(); - - float actualPageWidth = mediaBox.getWidth(); - float actualPageHeight = mediaBox.getHeight(); - - pdfDocument.close(); - - // Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842" - // for A4 - String[] dimensions = expectedPageSize.split("x"); - float expectedPageWidth = Float.parseFloat(dimensions[0]); - float expectedPageHeight = Float.parseFloat(dimensions[1]); - - // Checks if the actual page size matches the expected page size - return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight; - } - public static byte[] convertFromPdf( byte[] inputStream, String imageType, @@ -518,6 +442,82 @@ public class PdfUtils { return baos.toByteArray(); } + public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) + throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + String pdfText = ""; + + if (pagesToCheck == null || "all".equals(pagesToCheck)) { + pdfText = textStripper.getText(pdfDocument); + } else { + // remove whitespaces + pagesToCheck = pagesToCheck.replaceAll("\\s+", ""); + + String[] splitPoints = pagesToCheck.split(","); + for (String splitPoint : splitPoints) { + if (splitPoint.contains("-")) { + // Handle page ranges + String[] range = splitPoint.split("-"); + int startPage = Integer.parseInt(range[0]); + int endPage = Integer.parseInt(range[1]); + + for (int i = startPage; i <= endPage; i++) { + textStripper.setStartPage(i); + textStripper.setEndPage(i); + pdfText += textStripper.getText(pdfDocument); + } + } else { + // Handle individual page + int page = Integer.parseInt(splitPoint); + textStripper.setStartPage(page); + textStripper.setEndPage(page); + pdfText += textStripper.getText(pdfDocument); + } + } + } + + pdfDocument.close(); + + return pdfText.contains(text); + } + + public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) + throws IOException { + int actualPageCount = pdfDocument.getNumberOfPages(); + pdfDocument.close(); + + switch (comparator.toLowerCase()) { + case "greater": + return actualPageCount > pageCount; + case "equal": + return actualPageCount == pageCount; + case "less": + return actualPageCount < pageCount; + default: + throw new IllegalArgumentException( + "Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); + } + } + + public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException { + PDPage firstPage = pdfDocument.getPage(0); + PDRectangle mediaBox = firstPage.getMediaBox(); + + float actualPageWidth = mediaBox.getWidth(); + float actualPageHeight = mediaBox.getHeight(); + + pdfDocument.close(); + + // Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842" + // for A4 + String[] dimensions = expectedPageSize.split("x"); + float expectedPageWidth = Float.parseFloat(dimensions[0]); + float expectedPageHeight = Float.parseFloat(dimensions[1]); + + // Checks if the actual page size matches the expected page size + return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight; + } + /** Key for storing the dimensions of a rendered image in a map. */ private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {} diff --git a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java index 15eab3b24..d6d8afd6c 100644 --- a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java +++ b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java @@ -1,10 +1,6 @@ package stirling.software.SPDF.utils; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.InterruptedIOException; +import java.io.*; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -21,20 +17,17 @@ import stirling.software.SPDF.model.ApplicationProperties; @Slf4j public class ProcessExecutor { - private static ApplicationProperties applicationProperties = new ApplicationProperties(); - - public enum Processes { - LIBRE_OFFICE, - PDFTOHTML, - PYTHON_OPENCV, - WEASYPRINT, - INSTALL_APP, - CALIBRE, - TESSERACT, - QPDF - } - private static final Map instances = new ConcurrentHashMap<>(); + private static ApplicationProperties applicationProperties = new ApplicationProperties(); + private final Semaphore semaphore; + private final boolean liveUpdates; + private long timeoutDuration; + + private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) { + this.semaphore = new Semaphore(semaphoreLimit); + this.liveUpdates = liveUpdates; + this.timeoutDuration = timeout; + } public static ProcessExecutor getInstance(Processes processType) { return getInstance(processType, true); @@ -135,16 +128,6 @@ public class ProcessExecutor { }); } - private final Semaphore semaphore; - private final boolean liveUpdates; - private long timeoutDuration; - - private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) { - this.semaphore = new Semaphore(semaphoreLimit); - this.liveUpdates = liveUpdates; - this.timeoutDuration = timeout; - } - public ProcessExecutorResult runCommandWithOutputHandling(List command) throws IOException, InterruptedException { return runCommandWithOutputHandling(command, null); @@ -271,6 +254,17 @@ public class ProcessExecutor { return new ProcessExecutorResult(exitCode, messages); } + public enum Processes { + LIBRE_OFFICE, + PDFTOHTML, + PYTHON_OPENCV, + WEASYPRINT, + INSTALL_APP, + CALIBRE, + TESSERACT, + QPDF + } + public class ProcessExecutorResult { int rc; String messages; diff --git a/src/main/java/stirling/software/SPDF/utils/misc/CustomColorReplaceStrategy.java b/src/main/java/stirling/software/SPDF/utils/misc/CustomColorReplaceStrategy.java index de548881e..e2a5166fa 100644 --- a/src/main/java/stirling/software/SPDF/utils/misc/CustomColorReplaceStrategy.java +++ b/src/main/java/stirling/software/SPDF/utils/misc/CustomColorReplaceStrategy.java @@ -14,7 +14,10 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageTree; -import org.apache.pdfbox.pdmodel.font.*; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontFactory; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.pdfbox.text.TextPosition; import org.springframework.core.io.InputStreamResource; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 41cb3832d..d76eadf1b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,4 @@ multipart.enabled=true - logging.level.org.springframework=WARN logging.level.org.hibernate=WARN logging.level.org.eclipse.jetty=WARN @@ -8,52 +7,38 @@ logging.level.org.eclipse.jetty=WARN #logging.level.org.opensaml=DEBUG #logging.level.stirling.software.SPDF.config.security: DEBUG logging.level.com.zaxxer.hikari=WARN - spring.jpa.open-in-view=false - server.forward-headers-strategy=NATIVE - server.error.path=/error server.error.whitelabel.enabled=false server.error.include-stacktrace=always server.error.include-exception=true server.error.include-message=always - #logging.level.org.springframework.web=DEBUG #logging.level.org.springframework=DEBUG #logging.level.org.springframework.security=DEBUG - spring.servlet.multipart.max-file-size=2000MB spring.servlet.multipart.max-request-size=2000MB - server.servlet.session.tracking-modes=cookie server.servlet.context-path=${SYSTEM_ROOTURIPATH:/} - spring.devtools.restart.enabled=true spring.devtools.livereload.enabled=true spring.devtools.restart.exclude=stirling.software.SPDF.config.security/** - spring.thymeleaf.encoding=UTF-8 - spring.web.resources.mime-mappings.webmanifest=application/manifest+json - spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ #spring.thymeleaf.cache=false - spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=false spring.jpa.hibernate.ddl-auto=update -server.servlet.session.timeout: 30m +server.servlet.session.timeout:30m # Change the default URL path for OpenAPI JSON springdoc.api-docs.path=/v1/api-docs - # Set the URL of the OpenAPI JSON for the Swagger UI springdoc.swagger-ui.url=/v1/api-docs - - posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.host=https://eu.i.posthog.com diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 2d13578df..bcc330c6f 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -11,8 +11,8 @@ logs/invalid-auths.log - %d %p %c{1} [%thread] %m%n - + %d %p %c{1} [%thread] %m%n + @@ -21,12 +21,12 @@ - + logs/info.log - %d %p %c{1} [%thread] %m%n - + %d %p %c{1} [%thread] %m%n + @@ -43,7 +43,8 @@ - + diff --git a/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java b/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java index 350aef745..37f27918b 100644 --- a/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java +++ b/src/test/java/stirling/software/SPDF/SPdfApplicationTest.java @@ -15,6 +15,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.env.Environment; +import stirling.software.SPDF.UI.WebBrowser; import stirling.software.SPDF.model.ApplicationProperties; @ExtendWith(MockitoExtension.class) @@ -25,13 +26,12 @@ public class SPdfApplicationTest { @Mock private ApplicationProperties applicationProperties; - + @InjectMocks private SPdfApplication sPdfApplication; @BeforeEach public void setUp() { - sPdfApplication = new SPdfApplication(); sPdfApplication.setServerPortStatic("8080"); } diff --git a/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java b/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java index 4c91ffae9..03a6abb71 100644 --- a/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java +++ b/src/test/java/stirling/software/SPDF/controller/api/RearrangePagesPDFControllerTest.java @@ -8,6 +8,7 @@ import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.*; + import org.mockito.Mock; import org.mockito.MockitoAnnotations; import stirling.software.SPDF.service.CustomPDDocumentFactory; @@ -16,7 +17,10 @@ import java.util.Arrays; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull;import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.BeforeEach; + class RearrangePagesPDFControllerTest { @Mock @@ -53,7 +57,7 @@ class RearrangePagesPDFControllerTest { List newPageOrder = sut.oddEvenMerge(totalNumberOfPages); assertNotNull(newPageOrder, "Returning null instead of page order list"); - assertEquals(Arrays.asList(0,3,1,4,2), newPageOrder, "Page order doesn't match"); + assertEquals(Arrays.asList(0, 3, 1, 4, 2), newPageOrder, "Page order doesn't match"); } /** @@ -66,13 +70,14 @@ class RearrangePagesPDFControllerTest { List newPageOrder = sut.oddEvenMerge(totalNumberOfPages); assertNotNull(newPageOrder, "Returning null instead of page order list"); - assertEquals(Arrays.asList(0,3,1,4,2,5), newPageOrder, "Page order doesn't match"); + assertEquals(Arrays.asList(0, 3, 1, 4, 2, 5), newPageOrder, "Page order doesn't match"); } /** * Tests the behavior of the oddEvenMerge method with multiple test cases of multiple pages. + * * @param totalNumberOfPages The total number of pages in the document. - * @param expectedPageOrder The expected order of the pages after rearranging. + * @param expectedPageOrder The expected order of the pages after rearranging. */ @ParameterizedTest @CsvSource({ diff --git a/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java b/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java index ecf814b00..fa2c6e34d 100644 --- a/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java +++ b/src/test/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPdfTest.java @@ -4,13 +4,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.http.ResponseEntity; - -import stirling.software.SPDF.controller.api.RearrangePagesPDFController; import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.service.CustomPDDocumentFactory; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; public class ConvertWebsiteToPdfTest { diff --git a/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java b/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java index 585f677bc..804f1c165 100644 --- a/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java +++ b/src/test/java/stirling/software/SPDF/utils/FileToPdfTest.java @@ -1,12 +1,11 @@ package stirling.software.SPDF.utils; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest; import java.io.IOException; -import org.junit.jupiter.api.Test; - -import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest; +import static org.junit.jupiter.api.Assertions.assertThrows; public class FileToPdfTest { diff --git a/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java index 14f14c825..be63e8d36 100644 --- a/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/GeneralUtilsTest.java @@ -1,105 +1,108 @@ package stirling.software.SPDF.utils; -import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; + import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; + public class GeneralUtilsTest { - - @Test - void testParsePageListWithAll() { - List result = GeneralUtils.parsePageList(new String[]{"all"}, 5, false); - assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages."); - } - - @Test - void testParsePageListWithAllOneBased() { - List result = GeneralUtils.parsePageList(new String[]{"all"}, 5, true); - assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages."); - } - - @Test - void nFunc() { - List result = GeneralUtils.parsePageList(new String[]{"n"}, 5, true); - assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages."); - } - - @Test - void nFuncAdvanced() { - List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, true); - //skip 0 as not valid - assertEquals(List.of(4,8), result, "'All' keyword should return all pages."); - } + @Test + void testParsePageListWithAll() { + List result = GeneralUtils.parsePageList(new String[]{"all"}, 5, false); + assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages."); + } - @Test - void nFuncAdvancedZero() { - List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false); - //skip 0 as not valid - assertEquals(List.of(3,7), result, "'All' keyword should return all pages."); - } - - @Test - void nFuncAdvanced2() { - List result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, true); - // skip -1 as not valid - assertEquals(List.of(3,7), result, "4n-1 should do (0-1), (4-1), (8-1)"); - } - - @Test - void nFuncAdvanced3() { - List result = GeneralUtils.parsePageList(new String[]{"4n+1"}, 9, true); - assertEquals(List.of(1,5,9), result, "'All' keyword should return all pages."); - } - - - @Test - void nFuncAdvanced4() { - List result = GeneralUtils.parsePageList(new String[]{"3+2n"}, 9, true); - assertEquals(List.of(3,5,7,9), result, "'All' keyword should return all pages."); - } - - @Test - void nFuncAdvancedZerobased() { - List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false); - assertEquals(List.of(3,7), result, "'All' keyword should return all pages."); - } + @Test + void testParsePageListWithAllOneBased() { + List result = GeneralUtils.parsePageList(new String[]{"all"}, 5, true); + assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages."); + } - @Test - void nFuncAdvanced2Zerobased() { - List result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, false); - assertEquals(List.of(2,6), result, "'All' keyword should return all pages."); - } - @Test - void testParsePageListWithRangeOneBasedOutput() { - List result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, true); - assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly."); - } + @Test + void nFunc() { + List result = GeneralUtils.parsePageList(new String[]{"n"}, 5, true); + assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages."); + } - - @Test - void testParsePageListWithRangeZeroBaseOutput() { - List result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, false); - assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly."); - } - - - @Test - void testParsePageListWithRangeOneBasedOutputFull() { - List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, true); - assertEquals(List.of(1, 3, 7,8), result, "Range should be parsed correctly."); - } + @Test + void nFuncAdvanced() { + List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, true); + //skip 0 as not valid + assertEquals(List.of(4, 8), result, "'All' keyword should return all pages."); + } - @Test - void testParsePageListWithRangeOneBasedOutputFullOutOfRange() { - List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 5, true); - assertEquals(List.of(1, 3), result, "Range should be parsed correctly."); - } - @Test - void testParsePageListWithRangeZeroBaseOutputFull() { - List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, false); - assertEquals(List.of(0, 2, 6,7), result, "Range should be parsed correctly."); - } + @Test + void nFuncAdvancedZero() { + List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false); + //skip 0 as not valid + assertEquals(List.of(3, 7), result, "'All' keyword should return all pages."); + } + + @Test + void nFuncAdvanced2() { + List result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, true); + // skip -1 as not valid + assertEquals(List.of(3, 7), result, "4n-1 should do (0-1), (4-1), (8-1)"); + } + + @Test + void nFuncAdvanced3() { + List result = GeneralUtils.parsePageList(new String[]{"4n+1"}, 9, true); + assertEquals(List.of(1, 5, 9), result, "'All' keyword should return all pages."); + } + + + @Test + void nFuncAdvanced4() { + List result = GeneralUtils.parsePageList(new String[]{"3+2n"}, 9, true); + assertEquals(List.of(3, 5, 7, 9), result, "'All' keyword should return all pages."); + } + + @Test + void nFuncAdvancedZerobased() { + List result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false); + assertEquals(List.of(3, 7), result, "'All' keyword should return all pages."); + } + + @Test + void nFuncAdvanced2Zerobased() { + List result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, false); + assertEquals(List.of(2, 6), result, "'All' keyword should return all pages."); + } + + @Test + void testParsePageListWithRangeOneBasedOutput() { + List result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, true); + assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly."); + } + + + @Test + void testParsePageListWithRangeZeroBaseOutput() { + List result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, false); + assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly."); + } + + + @Test + void testParsePageListWithRangeOneBasedOutputFull() { + List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, true); + assertEquals(List.of(1, 3, 7, 8), result, "Range should be parsed correctly."); + } + + @Test + void testParsePageListWithRangeOneBasedOutputFullOutOfRange() { + List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 5, true); + assertEquals(List.of(1, 3), result, "Range should be parsed correctly."); + } + + @Test + void testParsePageListWithRangeZeroBaseOutputFull() { + List result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, false); + assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly."); + } } diff --git a/src/test/java/stirling/software/SPDF/utils/ImageProcessingUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/ImageProcessingUtilsTest.java index da990c1c4..af4bc22fb 100644 --- a/src/test/java/stirling/software/SPDF/utils/ImageProcessingUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/ImageProcessingUtilsTest.java @@ -2,8 +2,8 @@ package stirling.software.SPDF.utils; import org.junit.jupiter.api.Test; +import java.awt.*; import java.awt.image.BufferedImage; -import java.awt.Color; import static org.junit.jupiter.api.Assertions.*; @@ -13,14 +13,14 @@ public class ImageProcessingUtilsTest { void testConvertColorTypeToGreyscale() { BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); fillImageWithColor(sourceImage, Color.RED); - + BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "greyscale"); assertNotNull(convertedImage); assertEquals(BufferedImage.TYPE_BYTE_GRAY, convertedImage.getType()); assertEquals(sourceImage.getWidth(), convertedImage.getWidth()); assertEquals(sourceImage.getHeight(), convertedImage.getHeight()); - + // Check if a pixel is correctly converted to greyscale Color grey = new Color(convertedImage.getRGB(0, 0)); assertEquals(grey.getRed(), grey.getGreen()); @@ -31,14 +31,14 @@ public class ImageProcessingUtilsTest { void testConvertColorTypeToBlackWhite() { BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); fillImageWithColor(sourceImage, Color.RED); - + BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "blackwhite"); assertNotNull(convertedImage); assertEquals(BufferedImage.TYPE_BYTE_BINARY, convertedImage.getType()); assertEquals(sourceImage.getWidth(), convertedImage.getWidth()); assertEquals(sourceImage.getHeight(), convertedImage.getHeight()); - + // Check if a pixel is converted correctly (binary image will be either black or white) int rgb = convertedImage.getRGB(0, 0); assertTrue(rgb == Color.BLACK.getRGB() || rgb == Color.WHITE.getRGB()); @@ -48,7 +48,7 @@ public class ImageProcessingUtilsTest { void testConvertColorTypeToFullColor() { BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); fillImageWithColor(sourceImage, Color.RED); - + BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "fullcolor"); assertNotNull(convertedImage); @@ -59,7 +59,7 @@ public class ImageProcessingUtilsTest { void testConvertColorTypeInvalid() { BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB); fillImageWithColor(sourceImage, Color.RED); - + BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "invalidtype"); assertNotNull(convertedImage); diff --git a/src/test/java/stirling/software/SPDF/utils/PdfUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/PdfUtilsTest.java index a650e8918..e57e92035 100644 --- a/src/test/java/stirling/software/SPDF/utils/PdfUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/PdfUtilsTest.java @@ -1,11 +1,12 @@ package stirling.software.SPDF.utils; -import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -import stirling.software.SPDF.model.PdfMetadata; import java.io.IOException; import java.util.Collections; @@ -13,9 +14,6 @@ import java.util.HashSet; import java.util.Set; import static org.junit.jupiter.api.Assertions.*; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.cos.COSName; public class PdfUtilsTest { diff --git a/src/test/java/stirling/software/SPDF/utils/ProcessExecutorTest.java b/src/test/java/stirling/software/SPDF/utils/ProcessExecutorTest.java index cab78313a..10910b125 100644 --- a/src/test/java/stirling/software/SPDF/utils/ProcessExecutorTest.java +++ b/src/test/java/stirling/software/SPDF/utils/ProcessExecutorTest.java @@ -1,13 +1,13 @@ package stirling.software.SPDF.utils; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; public class ProcessExecutorTest { diff --git a/src/test/java/stirling/software/SPDF/utils/RequestUriUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/RequestUriUtilsTest.java index f18196030..7b586f472 100644 --- a/src/test/java/stirling/software/SPDF/utils/RequestUriUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/RequestUriUtilsTest.java @@ -1,10 +1,10 @@ package stirling.software.SPDF.utils; +import org.junit.jupiter.api.Test; + import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.Test; - public class RequestUriUtilsTest { @Test diff --git a/src/test/java/stirling/software/SPDF/utils/WebResponseUtilsTest.java b/src/test/java/stirling/software/SPDF/utils/WebResponseUtilsTest.java index 68ff619db..49ecab328 100644 --- a/src/test/java/stirling/software/SPDF/utils/WebResponseUtilsTest.java +++ b/src/test/java/stirling/software/SPDF/utils/WebResponseUtilsTest.java @@ -1,10 +1,5 @@ package stirling.software.SPDF.utils; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - import org.apache.pdfbox.pdmodel.PDDocument; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -13,6 +8,11 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + public class WebResponseUtilsTest { @Test @@ -55,7 +55,7 @@ public class WebResponseUtilsTest { assertNotNull(headers); assertEquals(MediaType.TEXT_PLAIN, headers.getContentType()); assertNotNull(headers.getContentDisposition()); - + } catch (IOException e) { fail("Exception thrown: " + e.getMessage()); } @@ -78,7 +78,7 @@ public class WebResponseUtilsTest { assertNotNull(headers); assertEquals(MediaType.TEXT_PLAIN, headers.getContentType()); assertNotNull(headers.getContentDisposition()); - + } catch (IOException e) { fail("Exception thrown: " + e.getMessage()); @@ -102,7 +102,7 @@ public class WebResponseUtilsTest { assertNotNull(headers); assertEquals(MediaType.APPLICATION_PDF, headers.getContentType()); assertNotNull(headers.getContentDisposition()); - + } catch (IOException e) { fail("Exception thrown: " + e.getMessage());