diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index ef3733c6..7388b5e4 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -1,81 +1,81 @@ -package stirling.software.SPDF; - -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Collections; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.core.env.Environment; -import org.springframework.scheduling.annotation.EnableScheduling; - -import jakarta.annotation.PostConstruct; -import stirling.software.SPDF.config.ConfigInitializer; -import stirling.software.SPDF.utils.GeneralUtils; - -@SpringBootApplication -@EnableScheduling -public class SPdfApplication { - - @Autowired private Environment env; - - @PostConstruct - public void init() { - // Check if the BROWSER_OPEN environment variable is set to true - String browserOpenEnv = env.getProperty("BROWSER_OPEN"); - boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true"); - - if (browserOpen) { - try { - String url = "http://localhost:" + getPort(); - - String os = System.getProperty("os.name").toLowerCase(); - Runtime rt = Runtime.getRuntime(); - if (os.contains("win")) { - // For Windows - rt.exec("rundll32 url.dll,FileProtocolHandler " + url); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - public static void main(String[] args) { - SpringApplication app = new SpringApplication(SPdfApplication.class); - app.addInitializers(new ConfigInitializer()); - if (Files.exists(Paths.get("configs/settings.yml"))) { - app.setDefaultProperties( - Collections.singletonMap( - "spring.config.additional-location", "file:configs/settings.yml")); - } else { - System.out.println( - "External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); - } - app.run(args); - - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - GeneralUtils.createDir("customFiles/static/"); - GeneralUtils.createDir("customFiles/templates/"); - - System.out.println("Stirling-PDF Started."); - - String url = "http://localhost:" + getPort(); - System.out.println("Navigate to " + url); - } - - public static String getPort() { - String port = System.getProperty("local.server.port"); - if (port == null || port.isEmpty()) { - port = "8080"; - } - return port; - } -} +package stirling.software.SPDF; + +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.EnableScheduling; + +import jakarta.annotation.PostConstruct; +import stirling.software.SPDF.config.ConfigInitializer; +import stirling.software.SPDF.utils.GeneralUtils; + +@SpringBootApplication +@EnableScheduling +public class SPdfApplication { + + @Autowired private Environment env; + + @PostConstruct + public void init() { + // Check if the BROWSER_OPEN environment variable is set to true + String browserOpenEnv = env.getProperty("BROWSER_OPEN"); + boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true"); + + if (browserOpen) { + try { + String url = "http://localhost:" + getPort(); + + String os = System.getProperty("os.name").toLowerCase(); + Runtime rt = Runtime.getRuntime(); + if (os.contains("win")) { + // For Windows + rt.exec("rundll32 url.dll,FileProtocolHandler " + url); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(SPdfApplication.class); + app.addInitializers(new ConfigInitializer()); + if (Files.exists(Paths.get("configs/settings.yml"))) { + app.setDefaultProperties( + Collections.singletonMap( + "spring.config.additional-location", "file:configs/settings.yml")); + } else { + System.out.println( + "External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); + } + app.run(args); + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + GeneralUtils.createDir("customFiles/static/"); + GeneralUtils.createDir("customFiles/templates/"); + + System.out.println("Stirling-PDF Started."); + + String url = "http://localhost:" + getPort(); + System.out.println("Navigate to " + url); + } + + public static String getPort() { + String port = System.getProperty("local.server.port"); + if (port == null || port.isEmpty()) { + port = "8080"; + } + return port; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 273de957..0411715f 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -1,60 +1,60 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import stirling.software.SPDF.model.ApplicationProperties; - -@Configuration -public class AppConfig { - - @Autowired ApplicationProperties applicationProperties; - - @Bean(name = "loginEnabled") - public boolean loginEnabled() { - return applicationProperties.getSecurity().getEnableLogin(); - } - - @Bean(name = "appName") - public String appName() { - String homeTitle = applicationProperties.getUi().getAppName(); - return (homeTitle != null) ? homeTitle : "Stirling PDF"; - } - - @Bean(name = "appVersion") - public String appVersion() { - String version = getClass().getPackage().getImplementationVersion(); - return (version != null) ? version : "0.0.0"; - } - - @Bean(name = "homeText") - public String homeText() { - return (applicationProperties.getUi().getHomeDescription() != null) - ? applicationProperties.getUi().getHomeDescription() - : "null"; - } - - @Bean(name = "navBarText") - public String navBarText() { - String defaultNavBar = - applicationProperties.getUi().getAppNameNavbar() != null - ? applicationProperties.getUi().getAppNameNavbar() - : applicationProperties.getUi().getAppName(); - return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; - } - - @Bean(name = "enableAlphaFunctionality") - public boolean enableAlphaFunctionality() { - return applicationProperties.getSystem().getEnableAlphaFunctionality() != null - ? applicationProperties.getSystem().getEnableAlphaFunctionality() - : false; - } - - @Bean(name = "rateLimit") - public boolean rateLimit() { - String appName = System.getProperty("rateLimit"); - if (appName == null) appName = System.getenv("rateLimit"); - return (appName != null) ? Boolean.valueOf(appName) : false; - } -} +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Configuration +public class AppConfig { + + @Autowired ApplicationProperties applicationProperties; + + @Bean(name = "loginEnabled") + public boolean loginEnabled() { + return applicationProperties.getSecurity().getEnableLogin(); + } + + @Bean(name = "appName") + public String appName() { + String homeTitle = applicationProperties.getUi().getAppName(); + return (homeTitle != null) ? homeTitle : "Stirling PDF"; + } + + @Bean(name = "appVersion") + public String appVersion() { + String version = getClass().getPackage().getImplementationVersion(); + return (version != null) ? version : "0.0.0"; + } + + @Bean(name = "homeText") + public String homeText() { + return (applicationProperties.getUi().getHomeDescription() != null) + ? applicationProperties.getUi().getHomeDescription() + : "null"; + } + + @Bean(name = "navBarText") + public String navBarText() { + String defaultNavBar = + applicationProperties.getUi().getAppNameNavbar() != null + ? applicationProperties.getUi().getAppNameNavbar() + : applicationProperties.getUi().getAppName(); + return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; + } + + @Bean(name = "enableAlphaFunctionality") + public boolean enableAlphaFunctionality() { + return applicationProperties.getSystem().getEnableAlphaFunctionality() != null + ? applicationProperties.getSystem().getEnableAlphaFunctionality() + : false; + } + + @Bean(name = "rateLimit") + public boolean rateLimit() { + String appName = System.getProperty("rateLimit"); + if (appName == null) appName = System.getenv("rateLimit"); + return (appName != null) ? Boolean.valueOf(appName) : false; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/Beans.java b/src/main/java/stirling/software/SPDF/config/Beans.java index 9230a0a0..03084b24 100644 --- a/src/main/java/stirling/software/SPDF/config/Beans.java +++ b/src/main/java/stirling/software/SPDF/config/Beans.java @@ -1,64 +1,64 @@ -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; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; -import org.springframework.web.servlet.i18n.SessionLocaleResolver; - -import stirling.software.SPDF.model.ApplicationProperties; - -@Configuration -public class Beans implements WebMvcConfigurer { - - @Autowired ApplicationProperties applicationProperties; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(localeChangeInterceptor()); - registry.addInterceptor(new CleanUrlInterceptor()); - } - - @Bean - public LocaleChangeInterceptor localeChangeInterceptor() { - LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); - lci.setParamName("lang"); - return lci; - } - - @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 - - 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 { - System.err.println( - "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); - } - } - } - - slr.setDefaultLocale(defaultLocale); - return slr; - } -} +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; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Configuration +public class Beans implements WebMvcConfigurer { + + @Autowired ApplicationProperties applicationProperties; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()); + registry.addInterceptor(new CleanUrlInterceptor()); + } + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); + lci.setParamName("lang"); + return lci; + } + + @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 + + 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 { + System.err.println( + "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); + } + } + } + + slr.setDefaultLocale(defaultLocale); + return slr; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index 472fb951..18393581 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -1,74 +1,74 @@ -package stirling.software.SPDF.config; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public class CleanUrlInterceptor implements HandlerInterceptor { - - private static final List ALLOWED_PARAMS = - Arrays.asList( - "lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); - - @Override - public boolean preHandle( - HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - Map parameters = new HashMap<>(); - - // Keep only the allowed parameters - String[] queryParameters = queryString.split("&"); - for (String param : queryParameters) { - String[] keyValue = param.split("="); - if (keyValue.length != 2) { - continue; - } - if (ALLOWED_PARAMS.contains(keyValue[0])) { - parameters.put(keyValue[0], keyValue[1]); - } - } - - // If there are any parameters that are not allowed - if (parameters.size() != queryParameters.length) { - // Construct new query string - StringBuilder newQueryString = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { - if (newQueryString.length() > 0) { - newQueryString.append("&"); - } - newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); - } - - // Redirect to the URL with only allowed query parameters - String redirectUrl = requestURI + "?" + newQueryString; - response.sendRedirect(redirectUrl); - return false; - } - } - return true; - } - - @Override - public void postHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler, - ModelAndView modelAndView) {} - - @Override - public void afterCompletion( - HttpServletRequest request, - HttpServletResponse response, - Object handler, - Exception ex) {} -} +package stirling.software.SPDF.config; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public class CleanUrlInterceptor implements HandlerInterceptor { + + private static final List ALLOWED_PARAMS = + Arrays.asList( + "lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String queryString = request.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + String requestURI = request.getRequestURI(); + Map parameters = new HashMap<>(); + + // Keep only the allowed parameters + String[] queryParameters = queryString.split("&"); + for (String param : queryParameters) { + String[] keyValue = param.split("="); + if (keyValue.length != 2) { + continue; + } + if (ALLOWED_PARAMS.contains(keyValue[0])) { + parameters.put(keyValue[0], keyValue[1]); + } + } + + // If there are any parameters that are not allowed + if (parameters.size() != queryParameters.length) { + // Construct new query string + StringBuilder newQueryString = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (newQueryString.length() > 0) { + newQueryString.append("&"); + } + newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); + } + + // Redirect to the URL with only allowed query parameters + String redirectUrl = requestURI + "?" + newQueryString; + response.sendRedirect(redirectUrl); + return false; + } + } + return true; + } + + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) {} + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) {} +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index ddba4623..593b70b4 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -1,231 +1,231 @@ -package stirling.software.SPDF.config; - -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import stirling.software.SPDF.model.ApplicationProperties; - -@Service -public class EndpointConfiguration { - private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); - private Map endpointStatuses = new ConcurrentHashMap<>(); - private Map> endpointGroups = new ConcurrentHashMap<>(); - - private final ApplicationProperties applicationProperties; - - @Autowired - public EndpointConfiguration(ApplicationProperties applicationProperties) { - this.applicationProperties = applicationProperties; - init(); - processEnvironmentConfigs(); - } - - public void enableEndpoint(String endpoint) { - endpointStatuses.put(endpoint, true); - } - - public void disableEndpoint(String endpoint) { - if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { - logger.info("Disabling {}", endpoint); - endpointStatuses.put(endpoint, false); - } - } - - public boolean isEndpointEnabled(String endpoint) { - if (endpoint.startsWith("/")) { - endpoint = endpoint.substring(1); - } - return endpointStatuses.getOrDefault(endpoint, true); - } - - public void addEndpointToGroup(String group, String endpoint) { - endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); - } - - public void enableGroup(String group) { - Set endpoints = endpointGroups.get(group); - if (endpoints != null) { - for (String endpoint : endpoints) { - enableEndpoint(endpoint); - } - } - } - - public void disableGroup(String group) { - Set endpoints = endpointGroups.get(group); - if (endpoints != null) { - for (String endpoint : endpoints) { - disableEndpoint(endpoint); - } - } - } - - public void init() { - // Adding endpoints to "PageOps" group - addEndpointToGroup("PageOps", "remove-pages"); - addEndpointToGroup("PageOps", "merge-pdfs"); - addEndpointToGroup("PageOps", "split-pdfs"); - addEndpointToGroup("PageOps", "pdf-organizer"); - addEndpointToGroup("PageOps", "rotate-pdf"); - addEndpointToGroup("PageOps", "multi-page-layout"); - addEndpointToGroup("PageOps", "scale-pages"); - addEndpointToGroup("PageOps", "adjust-contrast"); - addEndpointToGroup("PageOps", "crop"); - addEndpointToGroup("PageOps", "auto-split-pdf"); - addEndpointToGroup("PageOps", "extract-page"); - addEndpointToGroup("PageOps", "pdf-to-single-page"); - addEndpointToGroup("PageOps", "split-by-size-or-count"); - addEndpointToGroup("PageOps", "overlay-pdf"); - addEndpointToGroup("PageOps", "split-pdf-by-sections"); - - // Adding endpoints to "Convert" group - addEndpointToGroup("Convert", "pdf-to-img"); - addEndpointToGroup("Convert", "img-to-pdf"); - addEndpointToGroup("Convert", "pdf-to-pdfa"); - addEndpointToGroup("Convert", "file-to-pdf"); - addEndpointToGroup("Convert", "xlsx-to-pdf"); - addEndpointToGroup("Convert", "pdf-to-word"); - addEndpointToGroup("Convert", "pdf-to-presentation"); - addEndpointToGroup("Convert", "pdf-to-text"); - addEndpointToGroup("Convert", "pdf-to-html"); - addEndpointToGroup("Convert", "pdf-to-xml"); - addEndpointToGroup("Convert", "html-to-pdf"); - addEndpointToGroup("Convert", "url-to-pdf"); - addEndpointToGroup("Convert", "markdown-to-pdf"); - addEndpointToGroup("Convert", "pdf-to-csv"); - - // Adding endpoints to "Security" group - addEndpointToGroup("Security", "add-password"); - addEndpointToGroup("Security", "remove-password"); - addEndpointToGroup("Security", "change-permissions"); - addEndpointToGroup("Security", "add-watermark"); - addEndpointToGroup("Security", "cert-sign"); - addEndpointToGroup("Security", "sanitize-pdf"); - addEndpointToGroup("Security", "auto-redact"); - - // Adding endpoints to "Other" group - addEndpointToGroup("Other", "ocr-pdf"); - addEndpointToGroup("Other", "add-image"); - addEndpointToGroup("Other", "compress-pdf"); - addEndpointToGroup("Other", "extract-images"); - addEndpointToGroup("Other", "change-metadata"); - addEndpointToGroup("Other", "extract-image-scans"); - addEndpointToGroup("Other", "sign"); - addEndpointToGroup("Other", "flatten"); - addEndpointToGroup("Other", "repair"); - addEndpointToGroup("Other", "remove-blanks"); - addEndpointToGroup("Other", "remove-annotations"); - addEndpointToGroup("Other", "compare"); - addEndpointToGroup("Other", "add-page-numbers"); - addEndpointToGroup("Other", "auto-rename"); - addEndpointToGroup("Other", "get-info-on-pdf"); - addEndpointToGroup("Other", "show-javascript"); - - // CLI - addEndpointToGroup("CLI", "compress-pdf"); - addEndpointToGroup("CLI", "extract-image-scans"); - addEndpointToGroup("CLI", "remove-blanks"); - addEndpointToGroup("CLI", "repair"); - addEndpointToGroup("CLI", "pdf-to-pdfa"); - addEndpointToGroup("CLI", "file-to-pdf"); - addEndpointToGroup("CLI", "xlsx-to-pdf"); - addEndpointToGroup("CLI", "pdf-to-word"); - addEndpointToGroup("CLI", "pdf-to-presentation"); - addEndpointToGroup("CLI", "pdf-to-text"); - addEndpointToGroup("CLI", "pdf-to-html"); - addEndpointToGroup("CLI", "pdf-to-xml"); - addEndpointToGroup("CLI", "ocr-pdf"); - addEndpointToGroup("CLI", "html-to-pdf"); - addEndpointToGroup("CLI", "url-to-pdf"); - - // python - addEndpointToGroup("Python", "extract-image-scans"); - addEndpointToGroup("Python", "remove-blanks"); - addEndpointToGroup("Python", "html-to-pdf"); - addEndpointToGroup("Python", "url-to-pdf"); - - // openCV - addEndpointToGroup("OpenCV", "extract-image-scans"); - addEndpointToGroup("OpenCV", "remove-blanks"); - - // LibreOffice - addEndpointToGroup("LibreOffice", "repair"); - addEndpointToGroup("LibreOffice", "file-to-pdf"); - addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); - addEndpointToGroup("LibreOffice", "pdf-to-word"); - addEndpointToGroup("LibreOffice", "pdf-to-presentation"); - addEndpointToGroup("LibreOffice", "pdf-to-text"); - addEndpointToGroup("LibreOffice", "pdf-to-html"); - addEndpointToGroup("LibreOffice", "pdf-to-xml"); - - // OCRmyPDF - addEndpointToGroup("OCRmyPDF", "compress-pdf"); - addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); - addEndpointToGroup("OCRmyPDF", "ocr-pdf"); - - // Java - addEndpointToGroup("Java", "merge-pdfs"); - addEndpointToGroup("Java", "remove-pages"); - addEndpointToGroup("Java", "split-pdfs"); - addEndpointToGroup("Java", "pdf-organizer"); - addEndpointToGroup("Java", "rotate-pdf"); - addEndpointToGroup("Java", "pdf-to-img"); - addEndpointToGroup("Java", "img-to-pdf"); - addEndpointToGroup("Java", "add-password"); - addEndpointToGroup("Java", "remove-password"); - addEndpointToGroup("Java", "change-permissions"); - addEndpointToGroup("Java", "add-watermark"); - addEndpointToGroup("Java", "add-image"); - addEndpointToGroup("Java", "extract-images"); - addEndpointToGroup("Java", "change-metadata"); - addEndpointToGroup("Java", "cert-sign"); - addEndpointToGroup("Java", "multi-page-layout"); - addEndpointToGroup("Java", "scale-pages"); - addEndpointToGroup("Java", "add-page-numbers"); - addEndpointToGroup("Java", "auto-rename"); - addEndpointToGroup("Java", "auto-split-pdf"); - addEndpointToGroup("Java", "sanitize-pdf"); - addEndpointToGroup("Java", "crop"); - addEndpointToGroup("Java", "get-info-on-pdf"); - addEndpointToGroup("Java", "extract-page"); - addEndpointToGroup("Java", "pdf-to-single-page"); - addEndpointToGroup("Java", "markdown-to-pdf"); - addEndpointToGroup("Java", "show-javascript"); - addEndpointToGroup("Java", "auto-redact"); - addEndpointToGroup("Java", "pdf-to-csv"); - addEndpointToGroup("Java", "split-by-size-or-count"); - addEndpointToGroup("Java", "overlay-pdf"); - addEndpointToGroup("Java", "split-pdf-by-sections"); - - // Javascript - addEndpointToGroup("Javascript", "pdf-organizer"); - addEndpointToGroup("Javascript", "sign"); - addEndpointToGroup("Javascript", "compare"); - addEndpointToGroup("Javascript", "adjust-contrast"); - } - - private void processEnvironmentConfigs() { - List endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); - List groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); - - if (endpointsToRemove != null) { - for (String endpoint : endpointsToRemove) { - disableEndpoint(endpoint.trim()); - } - } - - if (groupsToRemove != null) { - for (String group : groupsToRemove) { - disableGroup(group.trim()); - } - } - } -} +package stirling.software.SPDF.config; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Service +public class EndpointConfiguration { + private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); + private Map endpointStatuses = new ConcurrentHashMap<>(); + private Map> endpointGroups = new ConcurrentHashMap<>(); + + private final ApplicationProperties applicationProperties; + + @Autowired + public EndpointConfiguration(ApplicationProperties applicationProperties) { + this.applicationProperties = applicationProperties; + init(); + processEnvironmentConfigs(); + } + + public void enableEndpoint(String endpoint) { + endpointStatuses.put(endpoint, true); + } + + public void disableEndpoint(String endpoint) { + if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { + logger.info("Disabling {}", endpoint); + endpointStatuses.put(endpoint, false); + } + } + + public boolean isEndpointEnabled(String endpoint) { + if (endpoint.startsWith("/")) { + endpoint = endpoint.substring(1); + } + return endpointStatuses.getOrDefault(endpoint, true); + } + + public void addEndpointToGroup(String group, String endpoint) { + endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); + } + + public void enableGroup(String group) { + Set endpoints = endpointGroups.get(group); + if (endpoints != null) { + for (String endpoint : endpoints) { + enableEndpoint(endpoint); + } + } + } + + public void disableGroup(String group) { + Set endpoints = endpointGroups.get(group); + if (endpoints != null) { + for (String endpoint : endpoints) { + disableEndpoint(endpoint); + } + } + } + + public void init() { + // Adding endpoints to "PageOps" group + addEndpointToGroup("PageOps", "remove-pages"); + addEndpointToGroup("PageOps", "merge-pdfs"); + addEndpointToGroup("PageOps", "split-pdfs"); + addEndpointToGroup("PageOps", "pdf-organizer"); + addEndpointToGroup("PageOps", "rotate-pdf"); + addEndpointToGroup("PageOps", "multi-page-layout"); + addEndpointToGroup("PageOps", "scale-pages"); + addEndpointToGroup("PageOps", "adjust-contrast"); + addEndpointToGroup("PageOps", "crop"); + addEndpointToGroup("PageOps", "auto-split-pdf"); + addEndpointToGroup("PageOps", "extract-page"); + addEndpointToGroup("PageOps", "pdf-to-single-page"); + addEndpointToGroup("PageOps", "split-by-size-or-count"); + addEndpointToGroup("PageOps", "overlay-pdf"); + addEndpointToGroup("PageOps", "split-pdf-by-sections"); + + // Adding endpoints to "Convert" group + addEndpointToGroup("Convert", "pdf-to-img"); + addEndpointToGroup("Convert", "img-to-pdf"); + addEndpointToGroup("Convert", "pdf-to-pdfa"); + addEndpointToGroup("Convert", "file-to-pdf"); + addEndpointToGroup("Convert", "xlsx-to-pdf"); + addEndpointToGroup("Convert", "pdf-to-word"); + addEndpointToGroup("Convert", "pdf-to-presentation"); + addEndpointToGroup("Convert", "pdf-to-text"); + addEndpointToGroup("Convert", "pdf-to-html"); + addEndpointToGroup("Convert", "pdf-to-xml"); + addEndpointToGroup("Convert", "html-to-pdf"); + addEndpointToGroup("Convert", "url-to-pdf"); + addEndpointToGroup("Convert", "markdown-to-pdf"); + addEndpointToGroup("Convert", "pdf-to-csv"); + + // Adding endpoints to "Security" group + addEndpointToGroup("Security", "add-password"); + addEndpointToGroup("Security", "remove-password"); + addEndpointToGroup("Security", "change-permissions"); + addEndpointToGroup("Security", "add-watermark"); + addEndpointToGroup("Security", "cert-sign"); + addEndpointToGroup("Security", "sanitize-pdf"); + addEndpointToGroup("Security", "auto-redact"); + + // Adding endpoints to "Other" group + addEndpointToGroup("Other", "ocr-pdf"); + addEndpointToGroup("Other", "add-image"); + addEndpointToGroup("Other", "compress-pdf"); + addEndpointToGroup("Other", "extract-images"); + addEndpointToGroup("Other", "change-metadata"); + addEndpointToGroup("Other", "extract-image-scans"); + addEndpointToGroup("Other", "sign"); + addEndpointToGroup("Other", "flatten"); + addEndpointToGroup("Other", "repair"); + addEndpointToGroup("Other", "remove-blanks"); + addEndpointToGroup("Other", "remove-annotations"); + addEndpointToGroup("Other", "compare"); + addEndpointToGroup("Other", "add-page-numbers"); + addEndpointToGroup("Other", "auto-rename"); + addEndpointToGroup("Other", "get-info-on-pdf"); + addEndpointToGroup("Other", "show-javascript"); + + // CLI + addEndpointToGroup("CLI", "compress-pdf"); + addEndpointToGroup("CLI", "extract-image-scans"); + addEndpointToGroup("CLI", "remove-blanks"); + addEndpointToGroup("CLI", "repair"); + addEndpointToGroup("CLI", "pdf-to-pdfa"); + addEndpointToGroup("CLI", "file-to-pdf"); + addEndpointToGroup("CLI", "xlsx-to-pdf"); + addEndpointToGroup("CLI", "pdf-to-word"); + addEndpointToGroup("CLI", "pdf-to-presentation"); + addEndpointToGroup("CLI", "pdf-to-text"); + addEndpointToGroup("CLI", "pdf-to-html"); + addEndpointToGroup("CLI", "pdf-to-xml"); + addEndpointToGroup("CLI", "ocr-pdf"); + addEndpointToGroup("CLI", "html-to-pdf"); + addEndpointToGroup("CLI", "url-to-pdf"); + + // python + addEndpointToGroup("Python", "extract-image-scans"); + addEndpointToGroup("Python", "remove-blanks"); + addEndpointToGroup("Python", "html-to-pdf"); + addEndpointToGroup("Python", "url-to-pdf"); + + // openCV + addEndpointToGroup("OpenCV", "extract-image-scans"); + addEndpointToGroup("OpenCV", "remove-blanks"); + + // LibreOffice + addEndpointToGroup("LibreOffice", "repair"); + addEndpointToGroup("LibreOffice", "file-to-pdf"); + addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); + addEndpointToGroup("LibreOffice", "pdf-to-word"); + addEndpointToGroup("LibreOffice", "pdf-to-presentation"); + addEndpointToGroup("LibreOffice", "pdf-to-text"); + addEndpointToGroup("LibreOffice", "pdf-to-html"); + addEndpointToGroup("LibreOffice", "pdf-to-xml"); + + // OCRmyPDF + addEndpointToGroup("OCRmyPDF", "compress-pdf"); + addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); + addEndpointToGroup("OCRmyPDF", "ocr-pdf"); + + // Java + addEndpointToGroup("Java", "merge-pdfs"); + addEndpointToGroup("Java", "remove-pages"); + addEndpointToGroup("Java", "split-pdfs"); + addEndpointToGroup("Java", "pdf-organizer"); + addEndpointToGroup("Java", "rotate-pdf"); + addEndpointToGroup("Java", "pdf-to-img"); + addEndpointToGroup("Java", "img-to-pdf"); + addEndpointToGroup("Java", "add-password"); + addEndpointToGroup("Java", "remove-password"); + addEndpointToGroup("Java", "change-permissions"); + addEndpointToGroup("Java", "add-watermark"); + addEndpointToGroup("Java", "add-image"); + addEndpointToGroup("Java", "extract-images"); + addEndpointToGroup("Java", "change-metadata"); + addEndpointToGroup("Java", "cert-sign"); + addEndpointToGroup("Java", "multi-page-layout"); + addEndpointToGroup("Java", "scale-pages"); + addEndpointToGroup("Java", "add-page-numbers"); + addEndpointToGroup("Java", "auto-rename"); + addEndpointToGroup("Java", "auto-split-pdf"); + addEndpointToGroup("Java", "sanitize-pdf"); + addEndpointToGroup("Java", "crop"); + addEndpointToGroup("Java", "get-info-on-pdf"); + addEndpointToGroup("Java", "extract-page"); + addEndpointToGroup("Java", "pdf-to-single-page"); + addEndpointToGroup("Java", "markdown-to-pdf"); + addEndpointToGroup("Java", "show-javascript"); + addEndpointToGroup("Java", "auto-redact"); + addEndpointToGroup("Java", "pdf-to-csv"); + addEndpointToGroup("Java", "split-by-size-or-count"); + addEndpointToGroup("Java", "overlay-pdf"); + addEndpointToGroup("Java", "split-pdf-by-sections"); + + // Javascript + addEndpointToGroup("Javascript", "pdf-organizer"); + addEndpointToGroup("Javascript", "sign"); + addEndpointToGroup("Javascript", "compare"); + addEndpointToGroup("Javascript", "adjust-contrast"); + } + + private void processEnvironmentConfigs() { + List endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); + List groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); + + if (endpointsToRemove != null) { + for (String endpoint : endpointsToRemove) { + disableEndpoint(endpoint.trim()); + } + } + + if (groupsToRemove != null) { + for (String group : groupsToRemove) { + disableGroup(group.trim()); + } + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index d408b9ea..81b50b84 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -1,26 +1,26 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class EndpointInterceptor implements HandlerInterceptor { - - @Autowired private EndpointConfiguration endpointConfiguration; - - @Override - public boolean preHandle( - HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String requestURI = request.getRequestURI(); - if (!endpointConfiguration.isEndpointEnabled(requestURI)) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); - return false; - } - return true; - } -} +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class EndpointInterceptor implements HandlerInterceptor { + + @Autowired private EndpointConfiguration endpointConfiguration; + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String requestURI = request.getRequestURI(); + if (!endpointConfiguration.isEndpointEnabled(requestURI)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); + return false; + } + return true; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java index 3877c566..ba216be7 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java @@ -1,25 +1,25 @@ -package stirling.software.SPDF.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.config.MeterFilterReply; - -@Configuration -public class MetricsConfig { - - @Bean - public MeterFilter meterFilter() { - return new MeterFilter() { - @Override - public MeterFilterReply accept(Meter.Id id) { - if (id.getName().equals("http.requests")) { - return MeterFilterReply.NEUTRAL; - } - return MeterFilterReply.DENY; - } - }; - } -} +package stirling.software.SPDF.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; + +@Configuration +public class MetricsConfig { + + @Bean + public MeterFilter meterFilter() { + return new MeterFilter() { + @Override + public MeterFilterReply accept(Meter.Id id) { + if (id.getName().equals("http.requests")) { + return MeterFilterReply.NEUTRAL; + } + return MeterFilterReply.DENY; + } + }; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 9207fd07..6f2751a9 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -1,64 +1,64 @@ -package stirling.software.SPDF.config; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class MetricsFilter extends OncePerRequestFilter { - - private final MeterRegistry meterRegistry; - - @Autowired - public MetricsFilter(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - String uri = request.getRequestURI(); - - // System.out.println("uri="+uri + ", method=" + request.getMethod() ); - // Ignore static resources - if (!(uri.startsWith("/js") - || uri.startsWith("/v1/api-docs") - || uri.endsWith("robots.txt") - || uri.startsWith("/images") - || uri.startsWith("/images") - || uri.endsWith(".png") - || uri.endsWith(".ico") - || uri.endsWith(".css") - || uri.endsWith(".map") - || uri.endsWith(".svg") - || uri.endsWith(".js") - || uri.contains("swagger") - || uri.startsWith("/api/v1/info") - || uri.startsWith("/site.webmanifest") - || uri.startsWith("/fonts") - || uri.startsWith("/pdfjs"))) { - - Counter counter = - Counter.builder("http.requests") - .tag("uri", uri) - .tag("method", request.getMethod()) - .register(meterRegistry); - - counter.increment(); - // System.out.println("Counted"); - } - - filterChain.doFilter(request, response); - } -} +package stirling.software.SPDF.config; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class MetricsFilter extends OncePerRequestFilter { + + private final MeterRegistry meterRegistry; + + @Autowired + public MetricsFilter(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); + + // System.out.println("uri="+uri + ", method=" + request.getMethod() ); + // Ignore static resources + if (!(uri.startsWith("/js") + || uri.startsWith("/v1/api-docs") + || uri.endsWith("robots.txt") + || uri.startsWith("/images") + || uri.startsWith("/images") + || uri.endsWith(".png") + || uri.endsWith(".ico") + || uri.endsWith(".css") + || uri.endsWith(".map") + || uri.endsWith(".svg") + || uri.endsWith(".js") + || uri.contains("swagger") + || uri.startsWith("/api/v1/info") + || uri.startsWith("/site.webmanifest") + || uri.startsWith("/fonts") + || uri.startsWith("/pdfjs"))) { + + Counter counter = + Counter.builder("http.requests") + .tag("uri", uri) + .tag("method", request.getMethod()) + .register(meterRegistry); + + counter.increment(); + // System.out.println("Counted"); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index a852bb1a..7194f4a2 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,53 +1,53 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; - -import stirling.software.SPDF.model.ApplicationProperties; - -@Configuration -public class OpenApiConfig { - - @Autowired ApplicationProperties applicationProperties; - - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - version = "1.0.0"; // default version if all else fails - } - - SecurityScheme apiKeyScheme = - new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .in(SecurityScheme.In.HEADER) - .name("X-API-KEY"); - if (!applicationProperties.getSecurity().getEnableLogin()) { - return new OpenAPI() - .components(new Components()) - .info( - new Info() - .title("Stirling PDF API") - .version(version) - .description( - "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); - } else { - return new OpenAPI() - .components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) - .info( - new Info() - .title("Stirling PDF API") - .version(version) - .description( - "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) - .addSecurityItem(new SecurityRequirement().addList("apiKey")); - } - } -} +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Configuration +public class OpenApiConfig { + + @Autowired ApplicationProperties applicationProperties; + + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "1.0.0"; // default version if all else fails + } + + SecurityScheme apiKeyScheme = + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .in(SecurityScheme.In.HEADER) + .name("X-API-KEY"); + if (!applicationProperties.getSecurity().getEnableLogin()) { + return new OpenAPI() + .components(new Components()) + .info( + new Info() + .title("Stirling PDF API") + .version(version) + .description( + "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); + } else { + return new OpenAPI() + .components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) + .info( + new Info() + .title("Stirling PDF API") + .version(version) + .description( + "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) + .addSecurityItem(new SecurityRequirement().addList("apiKey")); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java index 07644b30..737b47d5 100644 --- a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java +++ b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java @@ -1,18 +1,18 @@ -package stirling.software.SPDF.config; - -import java.time.LocalDateTime; - -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.stereotype.Component; - -@Component -public class StartupApplicationListener implements ApplicationListener { - - public static LocalDateTime startTime; - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - startTime = LocalDateTime.now(); - } -} +package stirling.software.SPDF.config; + +import java.time.LocalDateTime; + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +@Component +public class StartupApplicationListener implements ApplicationListener { + + public static LocalDateTime startTime; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + startTime = LocalDateTime.now(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 850f334b..eaadd251 100644 --- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,26 +1,26 @@ -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; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - @Autowired private EndpointInterceptor endpointInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(endpointInterceptor); - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - // Handler for external static resources - registry.addResourceHandler("/**") - .addResourceLocations("file:customFiles/static/", "classpath:/static/"); - // .setCachePeriod(0); // Optional: disable caching - } -} +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; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired private EndpointInterceptor endpointInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(endpointInterceptor); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Handler for external static resources + registry.addResourceHandler("/**") + .addResourceLocations("file:customFiles/static/", "classpath:/static/"); + // .setCachePeriod(0); // Optional: disable caching + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java index 6c2a05d3..cbdf7d26 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -1,49 +1,49 @@ -package stirling.software.SPDF.config.security; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.LockedException; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; -import org.springframework.stereotype.Component; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { - - @Autowired private final LoginAttemptService loginAttemptService; - - @Autowired - public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { - this.loginAttemptService = loginAttemptService; - } - - @Override - public void onAuthenticationFailure( - HttpServletRequest request, - HttpServletResponse response, - AuthenticationException exception) - throws IOException, ServletException { - String ip = request.getRemoteAddr(); - logger.error("Failed login attempt from IP: " + ip); - - String username = request.getParameter("username"); - if (loginAttemptService.loginAttemptCheck(username)) { - setDefaultFailureUrl("/login?error=locked"); - - } else { - if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { - setDefaultFailureUrl("/login?error=badcredentials"); - } else if (exception.getClass().isAssignableFrom(LockedException.class)) { - setDefaultFailureUrl("/login?error=locked"); - } - } - - super.onAuthenticationFailure(request, response, exception); - } -} +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.LockedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Autowired private final LoginAttemptService loginAttemptService; + + @Autowired + public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { + this.loginAttemptService = loginAttemptService; + } + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) + throws IOException, ServletException { + String ip = request.getRemoteAddr(); + logger.error("Failed login attempt from IP: " + ip); + + String username = request.getParameter("username"); + if (loginAttemptService.loginAttemptCheck(username)) { + setDefaultFailureUrl("/login?error=locked"); + + } else { + if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { + setDefaultFailureUrl("/login?error=badcredentials"); + } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + setDefaultFailureUrl("/login?error=locked"); + } + } + + super.onAuthenticationFailure(request, response, exception); + } +} 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 021cdc31..cd048eb9 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -1,57 +1,57 @@ -package stirling.software.SPDF.config.security; - -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; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import stirling.software.SPDF.model.Authority; -import stirling.software.SPDF.model.User; -import stirling.software.SPDF.repository.UserRepository; - -@Service -public class CustomUserDetailsService implements UserDetailsService { - - @Autowired private UserRepository userRepository; - - @Autowired private LoginAttemptService loginAttemptService; - - @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = - userRepository - .findByUsername(username) - .orElseThrow( - () -> - 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."); - } - - return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), - user.isEnabled(), - true, - true, - true, - getAuthorities(user.getAuthorities())); - } - - private Collection getAuthorities(Set authorities) { - return authorities.stream() - .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); - } -} +package stirling.software.SPDF.config.security; + +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; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.repository.UserRepository; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired private UserRepository userRepository; + + @Autowired private LoginAttemptService loginAttemptService; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User user = + userRepository + .findByUsername(username) + .orElseThrow( + () -> + 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."); + } + + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), + user.isEnabled(), + true, + true, + true, + getAuthorities(user.getAuthorities())); + } + + private Collection getAuthorities(Set authorities) { + return authorities.stream() + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); + } +} 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 bf1e3661..ca88dcb9 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -1,140 +1,140 @@ -package stirling.software.SPDF.config.security; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; -import org.springframework.security.web.savedrequest.NullRequestCache; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; - -import stirling.software.SPDF.repository.JPATokenRepositoryImpl; - -@Configuration -@EnableWebSecurity() -@EnableMethodSecurity -public class SecurityConfiguration { - - @Autowired private UserDetailsService userDetailsService; - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Autowired @Lazy private UserService userService; - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; - - @Autowired private UserAuthenticationFilter userAuthenticationFilter; - - @Autowired private LoginAttemptService loginAttemptService; - - @Autowired private FirstLoginFilter firstLoginFilter; - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - if (loginEnabledValue) { - - http.csrf(csrf -> csrf.disable()); - http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); - http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); - http.formLogin( - formLogin -> - formLogin - .loginPage("/login") - .successHandler( - new CustomAuthenticationSuccessHandler()) - .defaultSuccessUrl("/") - .failureHandler( - new CustomAuthenticationFailureHandler( - loginAttemptService)) - .permitAll()) - .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())) - .logout( - logout -> - logout.logoutRequestMatcher( - new AntPathRequestMatcher("/logout")) - .logoutSuccessUrl("/login?logout=true") - .invalidateHttpSession(true) // Invalidate session - .deleteCookies("JSESSIONID", "remember-me")) - .rememberMe( - rememberMeConfigurer -> - rememberMeConfigurer // Use the configurator directly - .key("uniqueAndSecret") - .tokenRepository(persistentTokenRepository()) - .tokenValiditySeconds(1209600) // 2 weeks - ) - .authorizeHttpRequests( - authz -> - authz.requestMatchers( - 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.endsWith(".svg") - || trimmedUri.startsWith( - "/register") - || trimmedUri.startsWith("/error") - || trimmedUri.startsWith("/images/") - || trimmedUri.startsWith("/public/") - || trimmedUri.startsWith("/css/") - || trimmedUri.startsWith("/js/") - || trimmedUri.startsWith( - "/api/v1/info/status"); - }) - .permitAll() - .anyRequest() - .authenticated()) - .userDetailsService(userDetailsService) - .authenticationProvider(authenticationProvider()); - } else { - http.csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); - } - - return http.build(); - } - - @Bean - public IPRateLimitingFilter rateLimitingFilter() { - int maxRequestsPerIp = 1000000; // Example limit TODO add config level - return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); - } - - @Bean - public DaoAuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(userDetailsService); - authProvider.setPasswordEncoder(passwordEncoder()); - return authProvider; - } - - @Bean - public PersistentTokenRepository persistentTokenRepository() { - return new JPATokenRepositoryImpl(); - } -} +package stirling.software.SPDF.config.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.savedrequest.NullRequestCache; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import stirling.software.SPDF.repository.JPATokenRepositoryImpl; + +@Configuration +@EnableWebSecurity() +@EnableMethodSecurity +public class SecurityConfiguration { + + @Autowired private UserDetailsService userDetailsService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Autowired @Lazy private UserService userService; + + @Autowired + @Qualifier("loginEnabled") + public boolean loginEnabledValue; + + @Autowired private UserAuthenticationFilter userAuthenticationFilter; + + @Autowired private LoginAttemptService loginAttemptService; + + @Autowired private FirstLoginFilter firstLoginFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + if (loginEnabledValue) { + + http.csrf(csrf -> csrf.disable()); + http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); + http.formLogin( + formLogin -> + formLogin + .loginPage("/login") + .successHandler( + new CustomAuthenticationSuccessHandler()) + .defaultSuccessUrl("/") + .failureHandler( + new CustomAuthenticationFailureHandler( + loginAttemptService)) + .permitAll()) + .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())) + .logout( + logout -> + logout.logoutRequestMatcher( + new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login?logout=true") + .invalidateHttpSession(true) // Invalidate session + .deleteCookies("JSESSIONID", "remember-me")) + .rememberMe( + rememberMeConfigurer -> + rememberMeConfigurer // Use the configurator directly + .key("uniqueAndSecret") + .tokenRepository(persistentTokenRepository()) + .tokenValiditySeconds(1209600) // 2 weeks + ) + .authorizeHttpRequests( + authz -> + authz.requestMatchers( + 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.endsWith(".svg") + || trimmedUri.startsWith( + "/register") + || trimmedUri.startsWith("/error") + || trimmedUri.startsWith("/images/") + || trimmedUri.startsWith("/public/") + || trimmedUri.startsWith("/css/") + || trimmedUri.startsWith("/js/") + || trimmedUri.startsWith( + "/api/v1/info/status"); + }) + .permitAll() + .anyRequest() + .authenticated()) + .userDetailsService(userDetailsService) + .authenticationProvider(authenticationProvider()); + } else { + http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); + } + + return http.build(); + } + + @Bean + public IPRateLimitingFilter rateLimitingFilter() { + int maxRequestsPerIp = 1000000; // Example limit TODO add config level + return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public PersistentTokenRepository persistentTokenRepository() { + return new JPATokenRepositoryImpl(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index d12fc72b..47423eb6 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -1,118 +1,118 @@ -package stirling.software.SPDF.config.security; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Lazy; -import org.springframework.http.HttpStatus; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -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; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import stirling.software.SPDF.model.ApiKeyAuthenticationToken; - -@Component -public class UserAuthenticationFilter extends OncePerRequestFilter { - - @Autowired private UserDetailsService userDetailsService; - - @Autowired @Lazy private UserService userService; - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - - if (!loginEnabledValue) { - // If login is not enabled, just pass all requests without authentication - filterChain.doFilter(request, response); - return; - } - String requestURI = request.getRequestURI(); - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - - // Check for API key in the request headers if no authentication exists - if (authentication == null || !authentication.isAuthenticated()) { - String apiKey = request.getHeader("X-API-Key"); - if (apiKey != null && !apiKey.trim().isEmpty()) { - try { - // Use API key to authenticate. This requires you to have an authentication - // provider for API keys. - UserDetails userDetails = userService.loadUserByApiKey(apiKey); - if (userDetails == null) { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.getWriter().write("Invalid API Key."); - return; - } - authentication = - new ApiKeyAuthenticationToken( - userDetails, apiKey, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(authentication); - } catch (AuthenticationException e) { - // If API key authentication fails, deny the request - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.getWriter().write("Invalid API Key."); - return; - } - } - } - - // If we still don't have any authentication, deny the request - if (authentication == null || !authentication.isAuthenticated()) { - String method = request.getMethod(); - String contextPath = request.getContextPath(); - - if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { - response.sendRedirect(contextPath + "/login"); // redirect to the login page - return; - } else { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.getWriter() - .write( - "Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); - return; - } - } - - filterChain.doFilter(request, response); - } - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { - String uri = request.getRequestURI(); - String contextPath = request.getContextPath(); - String[] permitAllPatterns = { - contextPath + "/login", - contextPath + "/register", - contextPath + "/error", - contextPath + "/images/", - contextPath + "/public/", - contextPath + "/css/", - contextPath + "/js/", - contextPath + "/pdfjs/", - contextPath + "/api/v1/info/status", - contextPath + "/site.webmanifest" - }; - - for (String pattern : permitAllPatterns) { - if (uri.startsWith(pattern) || uri.endsWith(".svg")) { - return true; - } - } - - return false; - } -} +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +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; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.ApiKeyAuthenticationToken; + +@Component +public class UserAuthenticationFilter extends OncePerRequestFilter { + + @Autowired private UserDetailsService userDetailsService; + + @Autowired @Lazy private UserService userService; + + @Autowired + @Qualifier("loginEnabled") + public boolean loginEnabledValue; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (!loginEnabledValue) { + // If login is not enabled, just pass all requests without authentication + filterChain.doFilter(request, response); + return; + } + String requestURI = request.getRequestURI(); + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + // Check for API key in the request headers if no authentication exists + if (authentication == null || !authentication.isAuthenticated()) { + String apiKey = request.getHeader("X-API-Key"); + if (apiKey != null && !apiKey.trim().isEmpty()) { + try { + // Use API key to authenticate. This requires you to have an authentication + // provider for API keys. + UserDetails userDetails = userService.loadUserByApiKey(apiKey); + if (userDetails == null) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Invalid API Key."); + return; + } + authentication = + new ApiKeyAuthenticationToken( + userDetails, apiKey, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (AuthenticationException e) { + // If API key authentication fails, deny the request + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Invalid API Key."); + return; + } + } + } + + // If we still don't have any authentication, deny the request + if (authentication == null || !authentication.isAuthenticated()) { + String method = request.getMethod(); + String contextPath = request.getContextPath(); + + if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { + response.sendRedirect(contextPath + "/login"); // redirect to the login page + return; + } else { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter() + .write( + "Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); + return; + } + } + + filterChain.doFilter(request, response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + String uri = request.getRequestURI(); + String contextPath = request.getContextPath(); + String[] permitAllPatterns = { + contextPath + "/login", + contextPath + "/register", + contextPath + "/error", + contextPath + "/images/", + contextPath + "/public/", + contextPath + "/css/", + contextPath + "/js/", + contextPath + "/pdfjs/", + contextPath + "/api/v1/info/status", + contextPath + "/site.webmanifest" + }; + + for (String pattern : permitAllPatterns) { + if (uri.startsWith(pattern) || uri.endsWith(".svg")) { + return true; + } + } + + return false; + } +} 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 6c315971..7b3b9b4e 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -1,143 +1,143 @@ -package stirling.software.SPDF.config.security; - -import java.io.IOException; -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; - -import io.github.bucket4j.Bandwidth; -import io.github.bucket4j.Bucket; -import io.github.bucket4j.ConsumptionProbe; -import io.github.bucket4j.Refill; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import stirling.software.SPDF.model.Role; - -@Component -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; - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - if (!rateLimit) { - // If rateLimit is not enabled, just pass all requests without rate limiting - 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 - } else { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.isAuthenticated()) { - UserDetails userDetails = (UserDetails) authentication.getPrincipal(); - 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( - userRole.getApiCallsPerDay(), - identifier, - apiBuckets, - request, - response, - filterChain); - } else { - // It's a Web UI call - processRequest( - userRole.getWebCallsPerDay(), - identifier, - webBuckets, - request, - response, - filterChain); - } - } - - private Role getRoleFromAuthentication(Authentication authentication) { - if (authentication != null && authentication.isAuthenticated()) { - for (GrantedAuthority authority : authentication.getAuthorities()) { - try { - return Role.fromString(authority.getAuthority()); - } catch (IllegalArgumentException ex) { - // Ignore and continue to next authority. - } - } - } - throw new IllegalStateException("User does not have a valid role."); - } - - private void processRequest( - int limitPerDay, - String identifier, - Map buckets, - HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) - 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", Long.toString(probe.getRemainingTokens())); - filterChain.doFilter(request, response); - } else { - long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; - response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); - response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); - response.getWriter().write("Rate limit exceeded for POST requests."); - } - } - - private Bucket createUserBucket(int limitPerDay) { - Bandwidth limit = - Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); - return Bucket.builder().addLimit(limit).build(); - } -} +package stirling.software.SPDF.config.security; + +import java.io.IOException; +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; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.ConsumptionProbe; +import io.github.bucket4j.Refill; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.Role; + +@Component +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; + + @Override + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (!rateLimit) { + // If rateLimit is not enabled, just pass all requests without rate limiting + 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 + } else { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + 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( + userRole.getApiCallsPerDay(), + identifier, + apiBuckets, + request, + response, + filterChain); + } else { + // It's a Web UI call + processRequest( + userRole.getWebCallsPerDay(), + identifier, + webBuckets, + request, + response, + filterChain); + } + } + + private Role getRoleFromAuthentication(Authentication authentication) { + if (authentication != null && authentication.isAuthenticated()) { + for (GrantedAuthority authority : authentication.getAuthorities()) { + try { + return Role.fromString(authority.getAuthority()); + } catch (IllegalArgumentException ex) { + // Ignore and continue to next authority. + } + } + } + throw new IllegalStateException("User does not have a valid role."); + } + + private void processRequest( + int limitPerDay, + String identifier, + Map buckets, + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + 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", Long.toString(probe.getRemainingTokens())); + filterChain.doFilter(request, response); + } else { + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill)); + response.getWriter().write("Rate limit exceeded for POST requests."); + } + } + + private Bucket createUserBucket(int limitPerDay) { + Bandwidth limit = + Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); + return Bucket.builder().addLimit(limit).build(); + } +} 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 986bb16f..60b3ebef 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,197 +1,197 @@ -package stirling.software.SPDF.config.security; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; -import stirling.software.SPDF.model.Authority; -import stirling.software.SPDF.model.Role; -import stirling.software.SPDF.model.User; -import stirling.software.SPDF.repository.UserRepository; - -@Service -public class UserService implements UserServiceInterface { - - @Autowired private UserRepository userRepository; - - @Autowired private PasswordEncoder passwordEncoder; - - public Authentication getAuthentication(String apiKey) { - User user = getUserByApiKey(apiKey); - if (user == null) { - 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) // user's authorities (roles/permissions) - ); - } - - private Collection getAuthorities(User user) { - // Convert each Authority object into a SimpleGrantedAuthority object. - return user.getAuthorities().stream() - .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); - } - - private String generateApiKey() { - String apiKey; - do { - apiKey = UUID.randomUUID().toString(); - } while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness - return apiKey; - } - - public User addApiKeyToUser(String username) { - User user = - userRepository - .findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - - user.setApiKey(generateApiKey()); - return userRepository.save(user); - } - - public User refreshApiKeyForUser(String username) { - return addApiKeyToUser(username); // reuse the add API key method for refreshing - } - - public String getApiKeyForUser(String username) { - User user = - userRepository - .findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - return user.getApiKey(); - } - - public boolean isValidApiKey(String apiKey) { - return userRepository.findByApiKey(apiKey) != null; - } - - public User getUserByApiKey(String apiKey) { - return userRepository.findByApiKey(apiKey); - } - - public UserDetails loadUserByApiKey(String apiKey) { - User userOptional = userRepository.findByApiKey(apiKey); - if (userOptional != null) { - User user = userOptional; - // Convert your User entity to a UserDetails object with authorities - return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), // you might not need this for API key auth - getAuthorities(user)); - } - return null; // or throw an exception - } - - public boolean validateApiKeyForUser(String username, String apiKey) { - Optional userOpt = userRepository.findByUsername(username); - return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); - } - - public void saveUser(String username, String password) { - User user = new User(); - user.setUsername(username); - user.setPassword(passwordEncoder.encode(password)); - user.setEnabled(true); - userRepository.save(user); - } - - public void saveUser(String username, String password, String role, boolean firstLogin) { - User user = new User(); - user.setUsername(username); - user.setPassword(passwordEncoder.encode(password)); - user.addAuthority(new Authority(role, user)); - user.setEnabled(true); - user.setFirstLogin(firstLogin); - userRepository.save(user); - } - - public void saveUser(String username, String password, String role) { - User user = new User(); - user.setUsername(username); - user.setPassword(passwordEncoder.encode(password)); - user.addAuthority(new Authority(role, user)); - user.setEnabled(true); - user.setFirstLogin(false); - userRepository.save(user); - } - - public void deleteUser(String username) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent()) { - for (Authority authority : userOpt.get().getAuthorities()) { - if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { - return; - } - } - userRepository.delete(userOpt.get()); - } - } - - public boolean usernameExists(String username) { - return userRepository.findByUsername(username).isPresent(); - } - - public boolean hasUsers() { - return userRepository.count() > 0; - } - - public void updateUserSettings(String username, Map updates) { - Optional userOpt = userRepository.findByUsername(username); - 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); - } - } - - public Optional findByUsername(String username) { - return userRepository.findByUsername(username); - } - - public void changeUsername(User user, String newUsername) { - user.setUsername(newUsername); - userRepository.save(user); - } - - public void changePassword(User user, String newPassword) { - user.setPassword(passwordEncoder.encode(newPassword)); - userRepository.save(user); - } - - public void changeFirstUse(User user, boolean firstUse) { - user.setFirstLogin(firstUse); - userRepository.save(user); - } - - public boolean isPasswordCorrect(User user, String currentPassword) { - return passwordEncoder.matches(currentPassword, user.getPassword()); - } -} +package stirling.software.SPDF.config.security; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.Role; +import stirling.software.SPDF.model.User; +import stirling.software.SPDF.repository.UserRepository; + +@Service +public class UserService implements UserServiceInterface { + + @Autowired private UserRepository userRepository; + + @Autowired private PasswordEncoder passwordEncoder; + + public Authentication getAuthentication(String apiKey) { + User user = getUserByApiKey(apiKey); + if (user == null) { + 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) // user's authorities (roles/permissions) + ); + } + + private Collection getAuthorities(User user) { + // Convert each Authority object into a SimpleGrantedAuthority object. + return user.getAuthorities().stream() + .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); + } + + private String generateApiKey() { + String apiKey; + do { + apiKey = UUID.randomUUID().toString(); + } while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness + return apiKey; + } + + public User addApiKeyToUser(String username) { + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + + user.setApiKey(generateApiKey()); + return userRepository.save(user); + } + + public User refreshApiKeyForUser(String username) { + return addApiKeyToUser(username); // reuse the add API key method for refreshing + } + + public String getApiKeyForUser(String username) { + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + return user.getApiKey(); + } + + public boolean isValidApiKey(String apiKey) { + return userRepository.findByApiKey(apiKey) != null; + } + + public User getUserByApiKey(String apiKey) { + return userRepository.findByApiKey(apiKey); + } + + public UserDetails loadUserByApiKey(String apiKey) { + User userOptional = userRepository.findByApiKey(apiKey); + if (userOptional != null) { + User user = userOptional; + // Convert your User entity to a UserDetails object with authorities + return new org.springframework.security.core.userdetails.User( + user.getUsername(), + user.getPassword(), // you might not need this for API key auth + getAuthorities(user)); + } + return null; // or throw an exception + } + + public boolean validateApiKeyForUser(String username, String apiKey) { + Optional userOpt = userRepository.findByUsername(username); + return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); + } + + public void saveUser(String username, String password) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.setEnabled(true); + userRepository.save(user); + } + + public void saveUser(String username, String password, String role, boolean firstLogin) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + user.setFirstLogin(firstLogin); + userRepository.save(user); + } + + public void saveUser(String username, String password, String role) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + user.setFirstLogin(false); + userRepository.save(user); + } + + public void deleteUser(String username) { + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + for (Authority authority : userOpt.get().getAuthorities()) { + if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { + return; + } + } + userRepository.delete(userOpt.get()); + } + } + + public boolean usernameExists(String username) { + return userRepository.findByUsername(username).isPresent(); + } + + public boolean hasUsers() { + return userRepository.count() > 0; + } + + public void updateUserSettings(String username, Map updates) { + Optional userOpt = userRepository.findByUsername(username); + 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); + } + } + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public void changeUsername(User user, String newUsername) { + user.setUsername(newUsername); + userRepository.save(user); + } + + public void changePassword(User user, String newPassword) { + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + } + + public void changeFirstUse(User user, boolean firstUse) { + user.setFirstLogin(firstUse); + userRepository.save(user); + } + + public boolean isPasswordCorrect(User user, String currentPassword) { + return passwordEncoder.matches(currentPassword, user.getPassword()); + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index bc4e2cce..71dca371 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -119,11 +119,10 @@ public class SplitPdfBySectionsController { // Set clipping area and position float translateX = -subPageWidth * i; float translateY = height - subPageHeight * (verticalDivisions - j); - - - //Code for google Docs pdfs.. - //float translateY = -subPageHeight * (verticalDivisions - 1 - j); - + + // Code for google Docs pdfs.. + // float translateY = -subPageHeight * (verticalDivisions - 1 - j); + contentStream.saveGraphicsState(); contentStream.addRect(0, 0, subPageWidth, subPageHeight); contentStream.clip(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index bf631c87..815018e8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -1,77 +1,77 @@ -package stirling.software.SPDF.controller.api.converters; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.http.ResponseEntity; -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; - -import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; -import stirling.software.SPDF.utils.GeneralUtils; -import stirling.software.SPDF.utils.ProcessExecutor; -import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@Tag(name = "Convert", description = "Convert APIs") -@RequestMapping("/api/v1/convert") -public class ConvertWebsiteToPDF { - - @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") - @Operation( - summary = "Convert a URL to a PDF", - description = - "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO") - public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) - throws IOException, InterruptedException { - String URL = request.getUrlInput(); - - // Validate the URL format - if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { - throw new IllegalArgumentException("Invalid URL format provided."); - } - Path tempOutputFile = null; - byte[] pdfBytes; - try { - // Prepare the output file path - tempOutputFile = Files.createTempFile("output_", ".pdf"); - - // Prepare the OCRmyPDF command - List command = new ArrayList<>(); - command.add("weasyprint"); - command.add(URL); - command.add(tempOutputFile.toString()); - - ProcessExecutorResult returnCode = - ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command); - - // Read the optimized PDF file - pdfBytes = Files.readAllBytes(tempOutputFile); - } finally { - // Clean up the temporary files - Files.delete(tempOutputFile); - } - // Convert URL to a safe filename - String outputFilename = convertURLToFileName(URL); - - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } - - private String convertURLToFileName(String url) { - String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); - if (safeName.length() > 50) { - safeName = safeName.substring(0, 50); // restrict to 50 characters - } - return safeName + ".pdf"; - } -} +package stirling.software.SPDF.controller.api.converters; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.http.ResponseEntity; +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; + +import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.ProcessExecutor; +import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +@Tag(name = "Convert", description = "Convert APIs") +@RequestMapping("/api/v1/convert") +public class ConvertWebsiteToPDF { + + @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") + @Operation( + summary = "Convert a URL to a PDF", + description = + "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO") + public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) + throws IOException, InterruptedException { + String URL = request.getUrlInput(); + + // Validate the URL format + if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { + throw new IllegalArgumentException("Invalid URL format provided."); + } + Path tempOutputFile = null; + byte[] pdfBytes; + try { + // Prepare the output file path + tempOutputFile = Files.createTempFile("output_", ".pdf"); + + // Prepare the OCRmyPDF command + List command = new ArrayList<>(); + command.add("weasyprint"); + command.add(URL); + command.add(tempOutputFile.toString()); + + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); + + // Read the optimized PDF file + pdfBytes = Files.readAllBytes(tempOutputFile); + } finally { + // Clean up the temporary files + Files.delete(tempOutputFile); + } + // Convert URL to a safe filename + String outputFilename = convertURLToFileName(URL); + + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } + + private String convertURLToFileName(String url) { + String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); + if (safeName.length() > 50) { + safeName = safeName.substring(0, 50); // restrict to 50 characters + } + return safeName + ".pdf"; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java index d159f974..967978a7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/filters/FilterController.java @@ -1,210 +1,210 @@ -package stirling.software.SPDF.controller.api.filters; - -import java.io.IOException; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.springframework.http.ResponseEntity; -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.multipart.MultipartFile; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; - -import stirling.software.SPDF.model.api.PDFComparisonAndCount; -import stirling.software.SPDF.model.api.PDFWithPageNums; -import stirling.software.SPDF.model.api.filter.ContainsTextRequest; -import stirling.software.SPDF.model.api.filter.FileSizeRequest; -import stirling.software.SPDF.model.api.filter.PageRotationRequest; -import stirling.software.SPDF.model.api.filter.PageSizeRequest; -import stirling.software.SPDF.utils.PdfUtils; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@RequestMapping("/api/v1/filter") -@Tag(name = "Filter", description = "Filter APIs") -public class FilterController { - - @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") - @Operation( - summary = "Checks if a PDF contains set text, returns true if does", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity containsText(@ModelAttribute ContainsTextRequest request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String text = request.getText(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasText(pdfDocument, pageNumber, text)) - return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, inputFile.getOriginalFilename()); - return null; - } - - // TODO - @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") - @Operation( - summary = "Checks if a PDF contains an image", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasImages(pdfDocument, pageNumber)) - return WebResponseUtils.pdfDocToWebResponse( - pdfDocument, inputFile.getOriginalFilename()); - return null; - } - - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") - @Operation( - summary = "Checks if a PDF is greater, less or equal to a setPageCount", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageCount(@ModelAttribute PDFComparisonAndCount request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageCount = request.getPageCount(); - String comparator = request.getComparator(); - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - int actualPageCount = document.getNumberOfPages(); - - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualPageCount > Integer.parseInt(pageCount); - break; - case "Equal": - valid = actualPageCount == Integer.parseInt(pageCount); - break; - case "Less": - valid = actualPageCount < Integer.parseInt(pageCount); - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - - if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } - - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") - @Operation( - summary = "Checks if a PDF is of a certain size", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageSize(@ModelAttribute PageSizeRequest request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String standardPageSize = request.getStandardPageSize(); - String comparator = request.getComparator(); - - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - - PDPage firstPage = document.getPage(0); - PDRectangle actualPageSize = firstPage.getMediaBox(); - - // Calculate the area of the actual page size - float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); - - // Get the standard size and calculate its area - PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); - float standardArea = standardSize.getWidth() * standardSize.getHeight(); - - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualArea > standardArea; - break; - case "Equal": - valid = actualArea == standardArea; - break; - case "Less": - valid = actualArea < standardArea; - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - - if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } - - @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") - @Operation( - summary = "Checks if a PDF is a set file size", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity fileSize(@ModelAttribute FileSizeRequest request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String fileSize = request.getFileSize(); - String comparator = request.getComparator(); - - // Get the file size - long actualFileSize = inputFile.getSize(); - - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualFileSize > Long.parseLong(fileSize); - break; - case "Equal": - valid = actualFileSize == Long.parseLong(fileSize); - break; - case "Less": - valid = actualFileSize < Long.parseLong(fileSize); - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - - if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } - - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") - @Operation( - summary = "Checks if a PDF is of a certain rotation", - description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageRotation(@ModelAttribute PageRotationRequest request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - int rotation = request.getRotation(); - String comparator = request.getComparator(); - - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - - // Get the rotation of the first page - PDPage firstPage = document.getPage(0); - int actualRotation = firstPage.getRotation(); - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualRotation > rotation; - break; - case "Equal": - valid = actualRotation == rotation; - break; - case "Less": - valid = actualRotation < rotation; - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } - - if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } -} +package stirling.software.SPDF.controller.api.filters; + +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.springframework.http.ResponseEntity; +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.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import stirling.software.SPDF.model.api.PDFComparisonAndCount; +import stirling.software.SPDF.model.api.PDFWithPageNums; +import stirling.software.SPDF.model.api.filter.ContainsTextRequest; +import stirling.software.SPDF.model.api.filter.FileSizeRequest; +import stirling.software.SPDF.model.api.filter.PageRotationRequest; +import stirling.software.SPDF.model.api.filter.PageSizeRequest; +import stirling.software.SPDF.utils.PdfUtils; +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/filter") +@Tag(name = "Filter", description = "Filter APIs") +public class FilterController { + + @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") + @Operation( + summary = "Checks if a PDF contains set text, returns true if does", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity containsText(@ModelAttribute ContainsTextRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String text = request.getText(); + String pageNumber = request.getPageNumbers(); + + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, inputFile.getOriginalFilename()); + return null; + } + + // TODO + @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") + @Operation( + summary = "Checks if a PDF contains an image", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageNumber = request.getPageNumbers(); + + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasImages(pdfDocument, pageNumber)) + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, inputFile.getOriginalFilename()); + return null; + } + + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") + @Operation( + summary = "Checks if a PDF is greater, less or equal to a setPageCount", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageCount(@ModelAttribute PDFComparisonAndCount request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageCount = request.getPageCount(); + String comparator = request.getComparator(); + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + int actualPageCount = document.getNumberOfPages(); + + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualPageCount > Integer.parseInt(pageCount); + break; + case "Equal": + valid = actualPageCount == Integer.parseInt(pageCount); + break; + case "Less": + valid = actualPageCount < Integer.parseInt(pageCount); + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } + + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") + @Operation( + summary = "Checks if a PDF is of a certain size", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageSize(@ModelAttribute PageSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String standardPageSize = request.getStandardPageSize(); + String comparator = request.getComparator(); + + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + + PDPage firstPage = document.getPage(0); + PDRectangle actualPageSize = firstPage.getMediaBox(); + + // Calculate the area of the actual page size + float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); + + // Get the standard size and calculate its area + PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); + float standardArea = standardSize.getWidth() * standardSize.getHeight(); + + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualArea > standardArea; + break; + case "Equal": + valid = actualArea == standardArea; + break; + case "Less": + valid = actualArea < standardArea; + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } + + @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") + @Operation( + summary = "Checks if a PDF is a set file size", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity fileSize(@ModelAttribute FileSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String fileSize = request.getFileSize(); + String comparator = request.getComparator(); + + // Get the file size + long actualFileSize = inputFile.getSize(); + + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualFileSize > Long.parseLong(fileSize); + break; + case "Equal": + valid = actualFileSize == Long.parseLong(fileSize); + break; + case "Less": + valid = actualFileSize < Long.parseLong(fileSize); + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } + + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") + @Operation( + summary = "Checks if a PDF is of a certain rotation", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageRotation(@ModelAttribute PageRotationRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + int rotation = request.getRotation(); + String comparator = request.getComparator(); + + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + + // Get the rotation of the first page + PDPage firstPage = document.getPage(0); + int actualRotation = firstPage.getRotation(); + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualRotation > rotation; + break; + case "Equal": + valid = actualRotation == rotation; + break; + case "Less": + valid = actualRotation < rotation; + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index 6c302524..0ae9a49e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -1,150 +1,150 @@ -package stirling.software.SPDF.controller.api.misc; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -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.multipart.MultipartFile; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; - -import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; -import stirling.software.SPDF.utils.GeneralUtils; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@RequestMapping("/api/v1/misc") -@Tag(name = "Misc", description = "Miscellaneous APIs") -public class PageNumbersController { - - private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); - - @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") - @Operation( - summary = "Add page numbers to a PDF document", - description = - "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) - throws IOException { - MultipartFile file = request.getFileInput(); - String customMargin = request.getCustomMargin(); - int position = request.getPosition(); - int startingNumber = request.getStartingNumber(); - String pagesToNumber = request.getPagesToNumber(); - String customText = request.getCustomText(); - int pageNumber = startingNumber; - byte[] fileBytes = file.getBytes(); - PDDocument document = PDDocument.load(fileBytes); - - float marginFactor; - switch (customMargin.toLowerCase()) { - case "small": - marginFactor = 0.02f; - break; - case "medium": - marginFactor = 0.035f; - break; - case "large": - marginFactor = 0.05f; - break; - case "x-large": - marginFactor = 0.075f; - break; - - default: - marginFactor = 0.035f; - break; - } - - float fontSize = 12.0f; - PDType1Font font = PDType1Font.HELVETICA; - if (pagesToNumber == null || pagesToNumber.length() == 0) { - pagesToNumber = "all"; - } - if (customText == null || customText.length() == 0) { - customText = "{n}"; - } - List pagesToNumberList = - GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); - - for (int i : pagesToNumberList) { - PDPage page = document.getPage(i); - PDRectangle pageSize = page.getMediaBox(); - - String text = - customText != null - ? customText - .replace("{n}", String.valueOf(pageNumber)) - .replace("{total}", String.valueOf(document.getNumberOfPages())) - .replace( - "{filename}", - file.getOriginalFilename() - .replaceFirst("[.][^.]+$", "")) - : String.valueOf(pageNumber); - - float x, y; - - int xGroup = (position - 1) % 3; - int yGroup = 2 - (position - 1) / 3; - - switch (xGroup) { - case 0: // left - x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); - break; - case 1: // center - x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); - break; - default: // right - x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth(); - break; - } - - switch (yGroup) { - case 0: // bottom - y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); - break; - case 1: // middle - y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); - break; - default: // top - y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); - break; - } - - PDPageContentStream contentStream = - new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true); - contentStream.beginText(); - contentStream.setFont(font, fontSize); - contentStream.newLineAtOffset(x, y); - contentStream.showText(text); - contentStream.endText(); - contentStream.close(); - - pageNumber++; - } - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - document.close(); - - return WebResponseUtils.bytesToWebResponse( - baos.toByteArray(), - file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", - MediaType.APPLICATION_PDF); - } -} +package stirling.software.SPDF.controller.api.misc; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +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.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; +import stirling.software.SPDF.utils.GeneralUtils; +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/misc") +@Tag(name = "Misc", description = "Miscellaneous APIs") +public class PageNumbersController { + + private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); + + @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") + @Operation( + summary = "Add page numbers to a PDF document", + description = + "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + String customMargin = request.getCustomMargin(); + int position = request.getPosition(); + int startingNumber = request.getStartingNumber(); + String pagesToNumber = request.getPagesToNumber(); + String customText = request.getCustomText(); + int pageNumber = startingNumber; + byte[] fileBytes = file.getBytes(); + PDDocument document = PDDocument.load(fileBytes); + + float marginFactor; + switch (customMargin.toLowerCase()) { + case "small": + marginFactor = 0.02f; + break; + case "medium": + marginFactor = 0.035f; + break; + case "large": + marginFactor = 0.05f; + break; + case "x-large": + marginFactor = 0.075f; + break; + + default: + marginFactor = 0.035f; + break; + } + + float fontSize = 12.0f; + PDType1Font font = PDType1Font.HELVETICA; + if (pagesToNumber == null || pagesToNumber.length() == 0) { + pagesToNumber = "all"; + } + if (customText == null || customText.length() == 0) { + customText = "{n}"; + } + List pagesToNumberList = + GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); + + for (int i : pagesToNumberList) { + PDPage page = document.getPage(i); + PDRectangle pageSize = page.getMediaBox(); + + String text = + customText != null + ? customText + .replace("{n}", String.valueOf(pageNumber)) + .replace("{total}", String.valueOf(document.getNumberOfPages())) + .replace( + "{filename}", + file.getOriginalFilename() + .replaceFirst("[.][^.]+$", "")) + : String.valueOf(pageNumber); + + float x, y; + + int xGroup = (position - 1) % 3; + int yGroup = 2 - (position - 1) / 3; + + switch (xGroup) { + case 0: // left + x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); + break; + case 1: // center + x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); + break; + default: // right + x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth(); + break; + } + + switch (yGroup) { + case 0: // bottom + y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); + break; + case 1: // middle + y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); + break; + default: // top + y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); + break; + } + + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true); + contentStream.beginText(); + contentStream.setFont(font, fontSize); + contentStream.newLineAtOffset(x, y); + contentStream.showText(text); + contentStream.endText(); + contentStream.close(); + + pageNumber++; + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + document.close(); + + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", + MediaType.APPLICATION_PDF); + } +} 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 31652e18..0a53daf0 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 @@ -1,133 +1,133 @@ -package stirling.software.SPDF.controller.api.pipeline; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -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.multipart.MultipartFile; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.swagger.v3.oas.annotations.tags.Tag; - -import stirling.software.SPDF.model.ApplicationProperties; -import stirling.software.SPDF.model.PipelineConfig; -import stirling.software.SPDF.model.api.HandleDataRequest; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@RequestMapping("/api/v1/pipeline") -@Tag(name = "Pipeline", description = "Pipeline APIs") -public class PipelineController { - - private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); - - final String watchedFoldersDir = "./pipeline/watchedFolders/"; - final String finishedFoldersDir = "./pipeline/finishedFolders/"; - @Autowired PipelineProcessor processor; - - @Autowired ApplicationProperties applicationProperties; - - @Autowired private ObjectMapper objectMapper; - - @PostMapping("/handleData") - public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) - throws JsonMappingException, JsonProcessingException { - if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { - return new ResponseEntity<>(HttpStatus.BAD_REQUEST); - } - - MultipartFile[] files = request.getFileInput(); - String jsonString = request.getJson(); - if (files == null) { - return null; - } - PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); - logger.info("Received POST request to /handleData with {} files", files.length); - try { - List inputFiles = processor.generateInputFiles(files); - if (inputFiles == null || inputFiles.size() == 0) { - return null; - } - List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); - if (outputFiles != null && outputFiles.size() == 1) { - // If there is only one file, return it directly - Resource singleFile = outputFiles.get(0); - InputStream is = singleFile.getInputStream(); - byte[] bytes = new byte[(int) singleFile.contentLength()]; - is.read(bytes); - is.close(); - - logger.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); - String baseName = originalFilename.replaceAll("\\.[^.]*$", ""); - String extension = originalFilename.replaceAll("^.*\\.", ""); - filename = baseName + "(" + count + ")." + extension; - filenameCount.put(originalFilename, count + 1); - } 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(); - - logger.info("Returning zipped file response..."); - return WebResponseUtils.boasToWebResponse( - baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); - } catch (Exception e) { - logger.error("Error handling data: ", e); - return null; - } - } -} +package stirling.software.SPDF.controller.api.pipeline; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +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.multipart.MultipartFile; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.swagger.v3.oas.annotations.tags.Tag; + +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.PipelineConfig; +import stirling.software.SPDF.model.api.HandleDataRequest; +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/pipeline") +@Tag(name = "Pipeline", description = "Pipeline APIs") +public class PipelineController { + + private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); + + final String watchedFoldersDir = "./pipeline/watchedFolders/"; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; + @Autowired PipelineProcessor processor; + + @Autowired ApplicationProperties applicationProperties; + + @Autowired private ObjectMapper objectMapper; + + @PostMapping("/handleData") + public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) + throws JsonMappingException, JsonProcessingException { + if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + MultipartFile[] files = request.getFileInput(); + String jsonString = request.getJson(); + if (files == null) { + return null; + } + PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); + logger.info("Received POST request to /handleData with {} files", files.length); + try { + List inputFiles = processor.generateInputFiles(files); + if (inputFiles == null || inputFiles.size() == 0) { + return null; + } + List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); + if (outputFiles != null && outputFiles.size() == 1) { + // If there is only one file, return it directly + Resource singleFile = outputFiles.get(0); + InputStream is = singleFile.getInputStream(); + byte[] bytes = new byte[(int) singleFile.contentLength()]; + is.read(bytes); + is.close(); + + logger.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); + String baseName = originalFilename.replaceAll("\\.[^.]*$", ""); + String extension = originalFilename.replaceAll("^.*\\.", ""); + filename = baseName + "(" + count + ")." + extension; + filenameCount.put(originalFilename, count + 1); + } 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(); + + logger.info("Returning zipped file response..."); + return WebResponseUtils.boasToWebResponse( + baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); + } catch (Exception e) { + logger.error("Error handling data: ", e); + return null; + } + } +} 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 21a33529..30ce1466 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 @@ -1,171 +1,171 @@ -package stirling.software.SPDF.controller.api.security; - -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.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.annotation.PDAnnotation; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; -import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; -import org.apache.pdfbox.pdmodel.interactive.form.PDField; -import org.springframework.http.ResponseEntity; -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.multipart.MultipartFile; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; - -import stirling.software.SPDF.model.api.security.SanitizePdfRequest; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@RequestMapping("/api/v1/security") -@Tag(name = "Security", description = "Security APIs") -public class SanitizeController { - - @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") - @Operation( - summary = "Sanitize a PDF file", - description = - "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") - public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) - throws IOException { - MultipartFile inputFile = request.getFileInput(); - boolean removeJavaScript = request.isRemoveJavaScript(); - boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); - boolean removeMetadata = request.isRemoveMetadata(); - boolean removeLinks = request.isRemoveLinks(); - boolean removeFonts = request.isRemoveFonts(); - - try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - if (removeJavaScript) { - sanitizeJavaScript(document); - } - - if (removeEmbeddedFiles) { - sanitizeEmbeddedFiles(document); - } - - if (removeMetadata) { - sanitizeMetadata(document); - } - - if (removeLinks) { - sanitizeLinks(document); - } - - if (removeFonts) { - sanitizeFonts(document); - } - - return WebResponseUtils.pdfDocToWebResponse( - document, - inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") - + "_sanitized.pdf"); - } - } - - private void sanitizeJavaScript(PDDocument document) throws IOException { - // Get the root dictionary (catalog) of the PDF - PDDocumentCatalog catalog = document.getDocumentCatalog(); - - // Get the Names dictionary - COSDictionary namesDict = - (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); - - if (namesDict != null) { - // Get the JavaScript dictionary - COSDictionary javaScriptDict = - (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); - - if (javaScriptDict != null) { - // Remove the JavaScript dictionary - namesDict.removeItem(COSName.getPDFName("JavaScript")); - } - } - - for (PDPage page : document.getPages()) { - for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationWidget) { - PDAnnotationWidget widget = (PDAnnotationWidget) annotation; - PDAction action = widget.getAction(); - if (action instanceof PDActionJavaScript) { - widget.setAction(null); - } - } - } - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm != null) { - for (PDField field : acroForm.getFields()) { - PDFormFieldAdditionalActions actions = field.getActions(); - if (actions != null) { - if (actions.getC() instanceof PDActionJavaScript) { - actions.setC(null); - } - if (actions.getF() instanceof PDActionJavaScript) { - actions.setF(null); - } - if (actions.getK() instanceof PDActionJavaScript) { - actions.setK(null); - } - if (actions.getV() instanceof PDActionJavaScript) { - actions.setV(null); - } - } - } - } - } - } - - private void sanitizeEmbeddedFiles(PDDocument document) { - PDPageTree allPages = document.getPages(); - - for (PDPage page : allPages) { - PDResources res = page.getResources(); - - // Remove embedded files from the PDF - res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); - } - } - - private void sanitizeMetadata(PDDocument document) { - PDMetadata metadata = document.getDocumentCatalog().getMetadata(); - if (metadata != null) { - document.getDocumentCatalog().setMetadata(null); - } - } - - private void sanitizeLinks(PDDocument document) throws IOException { - for (PDPage page : document.getPages()) { - for (PDAnnotation annotation : page.getAnnotations()) { - if (annotation instanceof PDAnnotationLink) { - PDAction action = ((PDAnnotationLink) annotation).getAction(); - if (action instanceof PDActionLaunch || action instanceof PDActionURI) { - ((PDAnnotationLink) annotation).setAction(null); - } - } - } - } - } - - private void sanitizeFonts(PDDocument document) { - for (PDPage page : document.getPages()) { - page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); - } - } -} +package stirling.software.SPDF.controller.api.security; + +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.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.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; +import org.springframework.http.ResponseEntity; +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.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import stirling.software.SPDF.model.api.security.SanitizePdfRequest; +import stirling.software.SPDF.utils.WebResponseUtils; + +@RestController +@RequestMapping("/api/v1/security") +@Tag(name = "Security", description = "Security APIs") +public class SanitizeController { + + @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") + @Operation( + summary = "Sanitize a PDF file", + description = + "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") + public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) + throws IOException { + MultipartFile inputFile = request.getFileInput(); + boolean removeJavaScript = request.isRemoveJavaScript(); + boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); + boolean removeMetadata = request.isRemoveMetadata(); + boolean removeLinks = request.isRemoveLinks(); + boolean removeFonts = request.isRemoveFonts(); + + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { + if (removeJavaScript) { + sanitizeJavaScript(document); + } + + if (removeEmbeddedFiles) { + sanitizeEmbeddedFiles(document); + } + + if (removeMetadata) { + sanitizeMetadata(document); + } + + if (removeLinks) { + sanitizeLinks(document); + } + + if (removeFonts) { + sanitizeFonts(document); + } + + return WebResponseUtils.pdfDocToWebResponse( + document, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_sanitized.pdf"); + } + } + + private void sanitizeJavaScript(PDDocument document) throws IOException { + // Get the root dictionary (catalog) of the PDF + PDDocumentCatalog catalog = document.getDocumentCatalog(); + + // Get the Names dictionary + COSDictionary namesDict = + (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); + + if (namesDict != null) { + // Get the JavaScript dictionary + COSDictionary javaScriptDict = + (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); + + if (javaScriptDict != null) { + // Remove the JavaScript dictionary + namesDict.removeItem(COSName.getPDFName("JavaScript")); + } + } + + for (PDPage page : document.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationWidget) { + PDAnnotationWidget widget = (PDAnnotationWidget) annotation; + PDAction action = widget.getAction(); + if (action instanceof PDActionJavaScript) { + widget.setAction(null); + } + } + } + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm != null) { + for (PDField field : acroForm.getFields()) { + PDFormFieldAdditionalActions actions = field.getActions(); + if (actions != null) { + if (actions.getC() instanceof PDActionJavaScript) { + actions.setC(null); + } + if (actions.getF() instanceof PDActionJavaScript) { + actions.setF(null); + } + if (actions.getK() instanceof PDActionJavaScript) { + actions.setK(null); + } + if (actions.getV() instanceof PDActionJavaScript) { + actions.setV(null); + } + } + } + } + } + } + + private void sanitizeEmbeddedFiles(PDDocument document) { + PDPageTree allPages = document.getPages(); + + for (PDPage page : allPages) { + PDResources res = page.getResources(); + + // Remove embedded files from the PDF + res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); + } + } + + private void sanitizeMetadata(PDDocument document) { + PDMetadata metadata = document.getDocumentCatalog().getMetadata(); + if (metadata != null) { + document.getDocumentCatalog().setMetadata(null); + } + } + + private void sanitizeLinks(PDDocument document) throws IOException { + for (PDPage page : document.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationLink) { + PDAction action = ((PDAnnotationLink) annotation).getAction(); + if (action instanceof PDActionLaunch || action instanceof PDActionURI) { + ((PDAnnotationLink) annotation).setAction(null); + } + } + } + } + } + + private void sanitizeFonts(PDDocument document) { + for (PDPage page : document.getPages()) { + page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index b34bac3c..16e42dec 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -1,113 +1,113 @@ -package stirling.software.SPDF.controller.web; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.servlet.ModelAndView; - -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Controller -@Tag(name = "Convert", description = "Convert APIs") -public class ConverterWebController { - - @GetMapping("/img-to-pdf") - @Hidden - public String convertImgToPdfForm(Model model) { - model.addAttribute("currentPage", "img-to-pdf"); - return "convert/img-to-pdf"; - } - - @GetMapping("/html-to-pdf") - @Hidden - public String convertHTMLToPdfForm(Model model) { - model.addAttribute("currentPage", "html-to-pdf"); - return "convert/html-to-pdf"; - } - - @GetMapping("/markdown-to-pdf") - @Hidden - public String convertMarkdownToPdfForm(Model model) { - model.addAttribute("currentPage", "markdown-to-pdf"); - return "convert/markdown-to-pdf"; - } - - @GetMapping("/url-to-pdf") - @Hidden - public String convertURLToPdfForm(Model model) { - model.addAttribute("currentPage", "url-to-pdf"); - return "convert/url-to-pdf"; - } - - @GetMapping("/pdf-to-img") - @Hidden - public String pdfToimgForm(Model model) { - model.addAttribute("currentPage", "pdf-to-img"); - return "convert/pdf-to-img"; - } - - @GetMapping("/file-to-pdf") - @Hidden - public String convertToPdfForm(Model model) { - model.addAttribute("currentPage", "file-to-pdf"); - return "convert/file-to-pdf"; - } - - // PDF TO...... - - @GetMapping("/pdf-to-html") - @Hidden - public ModelAndView pdfToHTML() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html"); - modelAndView.addObject("currentPage", "pdf-to-html"); - return modelAndView; - } - - @GetMapping("/pdf-to-presentation") - @Hidden - public ModelAndView pdfToPresentation() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation"); - modelAndView.addObject("currentPage", "pdf-to-presentation"); - return modelAndView; - } - - @GetMapping("/pdf-to-text") - @Hidden - public ModelAndView pdfToText() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text"); - modelAndView.addObject("currentPage", "pdf-to-text"); - return modelAndView; - } - - @GetMapping("/pdf-to-word") - @Hidden - public ModelAndView pdfToWord() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word"); - modelAndView.addObject("currentPage", "pdf-to-word"); - return modelAndView; - } - - @GetMapping("/pdf-to-xml") - @Hidden - public ModelAndView pdfToXML() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml"); - modelAndView.addObject("currentPage", "pdf-to-xml"); - return modelAndView; - } - - @GetMapping("/pdf-to-csv") - @Hidden - public ModelAndView pdfToCSV() { - ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv"); - modelAndView.addObject("currentPage", "pdf-to-csv"); - return modelAndView; - } - - @GetMapping("/pdf-to-pdfa") - @Hidden - public String pdfToPdfAForm(Model model) { - model.addAttribute("currentPage", "pdf-to-pdfa"); - return "convert/pdf-to-pdfa"; - } -} +package stirling.software.SPDF.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Controller +@Tag(name = "Convert", description = "Convert APIs") +public class ConverterWebController { + + @GetMapping("/img-to-pdf") + @Hidden + public String convertImgToPdfForm(Model model) { + model.addAttribute("currentPage", "img-to-pdf"); + return "convert/img-to-pdf"; + } + + @GetMapping("/html-to-pdf") + @Hidden + public String convertHTMLToPdfForm(Model model) { + model.addAttribute("currentPage", "html-to-pdf"); + return "convert/html-to-pdf"; + } + + @GetMapping("/markdown-to-pdf") + @Hidden + public String convertMarkdownToPdfForm(Model model) { + model.addAttribute("currentPage", "markdown-to-pdf"); + return "convert/markdown-to-pdf"; + } + + @GetMapping("/url-to-pdf") + @Hidden + public String convertURLToPdfForm(Model model) { + model.addAttribute("currentPage", "url-to-pdf"); + return "convert/url-to-pdf"; + } + + @GetMapping("/pdf-to-img") + @Hidden + public String pdfToimgForm(Model model) { + model.addAttribute("currentPage", "pdf-to-img"); + return "convert/pdf-to-img"; + } + + @GetMapping("/file-to-pdf") + @Hidden + public String convertToPdfForm(Model model) { + model.addAttribute("currentPage", "file-to-pdf"); + return "convert/file-to-pdf"; + } + + // PDF TO...... + + @GetMapping("/pdf-to-html") + @Hidden + public ModelAndView pdfToHTML() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-html"); + modelAndView.addObject("currentPage", "pdf-to-html"); + return modelAndView; + } + + @GetMapping("/pdf-to-presentation") + @Hidden + public ModelAndView pdfToPresentation() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-presentation"); + modelAndView.addObject("currentPage", "pdf-to-presentation"); + return modelAndView; + } + + @GetMapping("/pdf-to-text") + @Hidden + public ModelAndView pdfToText() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-text"); + modelAndView.addObject("currentPage", "pdf-to-text"); + return modelAndView; + } + + @GetMapping("/pdf-to-word") + @Hidden + public ModelAndView pdfToWord() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-word"); + modelAndView.addObject("currentPage", "pdf-to-word"); + return modelAndView; + } + + @GetMapping("/pdf-to-xml") + @Hidden + public ModelAndView pdfToXML() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-xml"); + modelAndView.addObject("currentPage", "pdf-to-xml"); + return modelAndView; + } + + @GetMapping("/pdf-to-csv") + @Hidden + public ModelAndView pdfToCSV() { + ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv"); + modelAndView.addObject("currentPage", "pdf-to-csv"); + return modelAndView; + } + + @GetMapping("/pdf-to-pdfa") + @Hidden + public String pdfToPdfAForm(Model model) { + model.addAttribute("currentPage", "pdf-to-pdfa"); + return "convert/pdf-to-pdfa"; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index ba67e1d7..68b16d89 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -1,69 +1,69 @@ -package stirling.software.SPDF.controller.web; - -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; - -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.tags.Tag; - -@Controller -@Tag(name = "Security", description = "Security APIs") -public class SecurityWebController { - - @GetMapping("/auto-redact") - @Hidden - public String autoRedactForm(Model model) { - model.addAttribute("currentPage", "auto-redact"); - return "security/auto-redact"; - } - - @GetMapping("/add-password") - @Hidden - public String addPasswordForm(Model model) { - model.addAttribute("currentPage", "add-password"); - return "security/add-password"; - } - - @GetMapping("/change-permissions") - @Hidden - public String permissionsForm(Model model) { - model.addAttribute("currentPage", "change-permissions"); - return "security/change-permissions"; - } - - @GetMapping("/remove-password") - @Hidden - public String removePasswordForm(Model model) { - model.addAttribute("currentPage", "remove-password"); - return "security/remove-password"; - } - - @GetMapping("/add-watermark") - @Hidden - public String addWatermarkForm(Model model) { - model.addAttribute("currentPage", "add-watermark"); - return "security/add-watermark"; - } - - @GetMapping("/cert-sign") - @Hidden - public String certSignForm(Model model) { - model.addAttribute("currentPage", "cert-sign"); - return "security/cert-sign"; - } - - @GetMapping("/sanitize-pdf") - @Hidden - public String sanitizeForm(Model model) { - model.addAttribute("currentPage", "sanitize-pdf"); - return "security/sanitize-pdf"; - } - - @GetMapping("/get-info-on-pdf") - @Hidden - public String getInfo(Model model) { - model.addAttribute("currentPage", "get-info-on-pdf"); - return "security/get-info-on-pdf"; - } -} +package stirling.software.SPDF.controller.web; + +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Controller +@Tag(name = "Security", description = "Security APIs") +public class SecurityWebController { + + @GetMapping("/auto-redact") + @Hidden + public String autoRedactForm(Model model) { + model.addAttribute("currentPage", "auto-redact"); + return "security/auto-redact"; + } + + @GetMapping("/add-password") + @Hidden + public String addPasswordForm(Model model) { + model.addAttribute("currentPage", "add-password"); + return "security/add-password"; + } + + @GetMapping("/change-permissions") + @Hidden + public String permissionsForm(Model model) { + model.addAttribute("currentPage", "change-permissions"); + return "security/change-permissions"; + } + + @GetMapping("/remove-password") + @Hidden + public String removePasswordForm(Model model) { + model.addAttribute("currentPage", "remove-password"); + return "security/remove-password"; + } + + @GetMapping("/add-watermark") + @Hidden + public String addWatermarkForm(Model model) { + model.addAttribute("currentPage", "add-watermark"); + return "security/add-watermark"; + } + + @GetMapping("/cert-sign") + @Hidden + public String certSignForm(Model model) { + model.addAttribute("currentPage", "cert-sign"); + return "security/cert-sign"; + } + + @GetMapping("/sanitize-pdf") + @Hidden + public String sanitizeForm(Model model) { + model.addAttribute("currentPage", "sanitize-pdf"); + return "security/sanitize-pdf"; + } + + @GetMapping("/get-info-on-pdf") + @Hidden + public String getInfo(Model model) { + model.addAttribute("currentPage", "get-info-on-pdf"); + return "security/get-info-on-pdf"; + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index a1e177e4..cdf00bf0 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -1,186 +1,186 @@ -package stirling.software.SPDF.utils; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -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.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.List; - -import org.springframework.web.multipart.MultipartFile; - -public class GeneralUtils { - - public static void deleteDirectory(Path path) throws IOException { - Files.walkFileTree( - path, - new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) - throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - } - - public static String convertToFileName(String name) { - String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); - if (safeName.length() > 50) { - safeName = safeName.substring(0, 50); - } - return safeName; - } - - public static boolean isValidURL(String urlStr) { - try { - new URL(urlStr); - return true; - } catch (MalformedURLException e) { - return false; - } - } - - public static File multipartToFile(MultipartFile multipart) throws IOException { - Path tempFile = Files.createTempFile("overlay-", ".pdf"); - try (InputStream in = multipart.getInputStream(); - FileOutputStream out = new FileOutputStream(tempFile.toFile())) { - byte[] buffer = new byte[1024]; - int bytesRead; - while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - } - } - return tempFile.toFile(); - } - - public static Long convertSizeToBytes(String sizeStr) { - if (sizeStr == null) { - return null; - } - - sizeStr = sizeStr.trim().toUpperCase(); - try { - if (sizeStr.endsWith("KB")) { - return (long) - (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); - } else if (sizeStr.endsWith("MB")) { - return (long) - (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) - * 1024 - * 1024); - } else if (sizeStr.endsWith("GB")) { - return (long) - (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) - * 1024 - * 1024 - * 1024); - } else if (sizeStr.endsWith("B")) { - return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); - } else { - // Assume MB if no unit is specified - return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); - } - } catch (NumberFormatException e) { - // The numeric part of the input string cannot be parsed, handle this case - } - - return null; - } - - public static List parsePageString(String pageOrder, int totalPages) { - return parsePageList(pageOrder.split(","), totalPages); - } - - public static List parsePageList(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); - - // loop through the page order array - for (String element : pageOrderArr) { - if (element.equalsIgnoreCase("all")) { - for (int i = 0; i < totalPages; i++) { - newPageOrder.add(i); - } - // As all pages are already added, no need to check further - break; - } else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { - // Handle page order as a function - int coefficient = 0; - int constant = 0; - boolean coefficientExists = false; - boolean constantExists = false; - - if (element.contains("n")) { - String[] parts = element.split("n"); - if (!parts[0].equals("") && parts[0] != null) { - coefficient = Integer.parseInt(parts[0]); - coefficientExists = true; - } - if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { - constant = Integer.parseInt(parts[1]); - constantExists = true; - } - } else if (element.contains("+")) { - constant = Integer.parseInt(element.replace("+", "")); - constantExists = true; - } - - for (int i = 1; i <= totalPages; i++) { - int pageNum = coefficientExists ? coefficient * i : i; - pageNum += constantExists ? constant : 0; - - if (pageNum <= totalPages && pageNum > 0) { - newPageOrder.add(pageNum - 1); - } - } - } else if (element.contains("-")) { - // split the range into start and end page - String[] range = element.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - // check if the end page is greater than total pages - if (end > totalPages) { - end = totalPages; - } - // loop through the range of pages - for (int j = start; j <= end; j++) { - // print the current index - newPageOrder.add(j - 1); - } - } else { - // if the element is a single page - newPageOrder.add(Integer.parseInt(element) - 1); - } - } - - return newPageOrder; - } - - public static boolean createDir(String path) { - Path folder = Paths.get(path); - if (!Files.exists(folder)) { - try { - Files.createDirectories(folder); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - return true; - } -} +package stirling.software.SPDF.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +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.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.multipart.MultipartFile; + +public class GeneralUtils { + + public static void deleteDirectory(Path path) throws IOException { + Files.walkFileTree( + path, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + + public static String convertToFileName(String name) { + String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); + if (safeName.length() > 50) { + safeName = safeName.substring(0, 50); + } + return safeName; + } + + public static boolean isValidURL(String urlStr) { + try { + new URL(urlStr); + return true; + } catch (MalformedURLException e) { + return false; + } + } + + public static File multipartToFile(MultipartFile multipart) throws IOException { + Path tempFile = Files.createTempFile("overlay-", ".pdf"); + try (InputStream in = multipart.getInputStream(); + FileOutputStream out = new FileOutputStream(tempFile.toFile())) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) != -1) { + out.write(buffer, 0, bytesRead); + } + } + return tempFile.toFile(); + } + + public static Long convertSizeToBytes(String sizeStr) { + if (sizeStr == null) { + return null; + } + + sizeStr = sizeStr.trim().toUpperCase(); + try { + if (sizeStr.endsWith("KB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); + } else if (sizeStr.endsWith("MB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024); + } else if (sizeStr.endsWith("GB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024 + * 1024); + } else if (sizeStr.endsWith("B")) { + return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); + } else { + // Assume MB if no unit is specified + return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); + } + } catch (NumberFormatException e) { + // The numeric part of the input string cannot be parsed, handle this case + } + + return null; + } + + public static List parsePageString(String pageOrder, int totalPages) { + return parsePageList(pageOrder.split(","), totalPages); + } + + public static List parsePageList(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); + + // loop through the page order array + for (String element : pageOrderArr) { + if (element.equalsIgnoreCase("all")) { + for (int i = 0; i < totalPages; i++) { + newPageOrder.add(i); + } + // As all pages are already added, no need to check further + break; + } else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { + // Handle page order as a function + int coefficient = 0; + int constant = 0; + boolean coefficientExists = false; + boolean constantExists = false; + + if (element.contains("n")) { + String[] parts = element.split("n"); + if (!parts[0].equals("") && parts[0] != null) { + coefficient = Integer.parseInt(parts[0]); + coefficientExists = true; + } + if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { + constant = Integer.parseInt(parts[1]); + constantExists = true; + } + } else if (element.contains("+")) { + constant = Integer.parseInt(element.replace("+", "")); + constantExists = true; + } + + for (int i = 1; i <= totalPages; i++) { + int pageNum = coefficientExists ? coefficient * i : i; + pageNum += constantExists ? constant : 0; + + if (pageNum <= totalPages && pageNum > 0) { + newPageOrder.add(pageNum - 1); + } + } + } else if (element.contains("-")) { + // split the range into start and end page + String[] range = element.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + // check if the end page is greater than total pages + if (end > totalPages) { + end = totalPages; + } + // loop through the range of pages + for (int j = start; j <= end; j++) { + // print the current index + newPageOrder.add(j - 1); + } + } else { + // if the element is a single page + newPageOrder.add(Integer.parseInt(element) - 1); + } + } + + return newPageOrder; + } + + public static boolean createDir(String path) { + Path folder = Paths.get(path); + if (!Files.exists(folder)) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + return true; + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 677bafd1..79a47864 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -1,400 +1,400 @@ -package stirling.software.SPDF.utils; - -import java.awt.Graphics; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.ImageOutputStream; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; -import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.rendering.ImageType; -import org.apache.pdfbox.rendering.PDFRenderer; -import org.apache.pdfbox.text.PDFTextStripper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.web.multipart.MultipartFile; - -import stirling.software.SPDF.pdf.ImageFinder; - -public class PdfUtils { - - private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); - - public static PDRectangle textToPageSize(String size) { - switch (size.toUpperCase()) { - case "A0": - return PDRectangle.A0; - case "A1": - return PDRectangle.A1; - case "A2": - return PDRectangle.A2; - case "A3": - return PDRectangle.A3; - case "A4": - return PDRectangle.A4; - case "A5": - return PDRectangle.A5; - case "A6": - return PDRectangle.A6; - case "LETTER": - return PDRectangle.LETTER; - case "LEGAL": - return PDRectangle.LEGAL; - default: - throw new IllegalArgumentException("Invalid standard page size: " + size); - } - } - - public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { - String[] pageOrderArr = pagesToCheck.split(","); - List pageList = - GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); - - for (int pageNumber : pageList) { - PDPage page = document.getPage(pageNumber); - if (hasImagesOnPage(page)) { - return true; - } - } - - return false; - } - - public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) - throws IOException { - String[] pageOrderArr = pageNumbersToCheck.split(","); - List pageList = - GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); - - for (int pageNumber : pageList) { - PDPage page = document.getPage(pageNumber); - if (hasTextOnPage(page, phrase)) { - return true; - } - } - - return false; - } - - public static boolean hasImagesOnPage(PDPage page) throws IOException { - ImageFinder imageFinder = new ImageFinder(page); - imageFinder.processPage(page); - return imageFinder.hasImages(); - } - - public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { - PDFTextStripper textStripper = new PDFTextStripper(); - PDDocument tempDoc = new PDDocument(); - tempDoc.addPage(page); - String pageText = textStripper.getText(tempDoc); - tempDoc.close(); - return pageText.contains(phrase); - } - - public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) - throws IOException { - PDFTextStripper textStripper = new PDFTextStripper(); - String pdfText = ""; - - if (pagesToCheck == null || pagesToCheck.equals("all")) { - 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, - ImageType colorType, - boolean singleImage, - int DPI, - String filename) - throws IOException, Exception { - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { - PDFRenderer pdfRenderer = new PDFRenderer(document); - int pageCount = document.getNumberOfPages(); - - // Create a ByteArrayOutputStream to save the image(s) to - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - if (singleImage) { - if (imageType.toLowerCase().equals("tiff") - || imageType.toLowerCase().equals("tif")) { - // Write the images to the output stream as a TIFF with multiple frames - ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next(); - ImageWriteParam param = writer.getDefaultWriteParam(); - param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - param.setCompressionType("ZLib"); - param.setCompressionQuality(1.0f); - - try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) { - writer.setOutput(ios); - writer.prepareWriteSequence(null); - - for (int i = 0; i < pageCount; ++i) { - BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); - writer.writeToSequence(new IIOImage(image, null, null), param); - } - - writer.endWriteSequence(); - } - - writer.dispose(); - } else { - // Combine all images into a single big image - BufferedImage image = pdfRenderer.renderImageWithDPI(0, DPI, colorType); - BufferedImage combined = - new BufferedImage( - image.getWidth(), - image.getHeight() * pageCount, - BufferedImage.TYPE_INT_RGB); - Graphics g = combined.getGraphics(); - - for (int i = 0; i < pageCount; ++i) { - if (i != 0) { - image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); - } - g.drawImage(image, 0, i * image.getHeight(), null); - } - - // Write the image to the output stream - ImageIO.write(combined, imageType, baos); - } - - // Log that the image was successfully written to the byte array - logger.info("Image successfully written to byte array"); - } else { - // Zip the images and return as byte array - try (ZipOutputStream zos = new ZipOutputStream(baos)) { - for (int i = 0; i < pageCount; ++i) { - BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); - try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) { - ImageIO.write(image, imageType, baosImage); - - // Add the image to the zip file - zos.putNextEntry( - new ZipEntry( - String.format( - filename + "_%d.%s", - i + 1, - imageType.toLowerCase()))); - zos.write(baosImage.toByteArray()); - } - } - // Log that the images were successfully written to the byte array - logger.info("Images successfully written to byte array as a zip"); - } - } - return baos.toByteArray(); - } catch (IOException e) { - // Log an error message if there is an issue converting the PDF to an image - logger.error("Error converting PDF to image", e); - throw e; - } - } - - public static byte[] imageToPdf( - MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) - throws IOException { - try (PDDocument doc = new PDDocument()) { - for (MultipartFile file : files) { - String contentType = file.getContentType(); - String originalFilename = file.getOriginalFilename(); - if (originalFilename != null - && (originalFilename.toLowerCase().endsWith(".tiff") - || originalFilename.toLowerCase().endsWith(".tif"))) { - ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next(); - reader.setInput(ImageIO.createImageInputStream(file.getInputStream())); - int numPages = reader.getNumImages(true); - for (int i = 0; i < numPages; i++) { - BufferedImage pageImage = reader.read(i); - BufferedImage convertedImage = - ImageProcessingUtils.convertColorType(pageImage, colorType); - PDImageXObject pdImage = - LosslessFactory.createFromImage(doc, convertedImage); - addImageToDocument(doc, pdImage, fitOption, autoRotate); - } - } else { - BufferedImage image = ImageIO.read(file.getInputStream()); - BufferedImage convertedImage = - ImageProcessingUtils.convertColorType(image, colorType); - // Use JPEGFactory if it's JPEG since JPEG is lossy - PDImageXObject pdImage = - (contentType != null && contentType.equals("image/jpeg")) - ? JPEGFactory.createFromImage(doc, convertedImage) - : LosslessFactory.createFromImage(doc, convertedImage); - addImageToDocument(doc, pdImage, fitOption, autoRotate); - } - } - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - doc.save(byteArrayOutputStream); - logger.info("PDF successfully saved to byte array"); - return byteArrayOutputStream.toByteArray(); - } - } - - private static void addImageToDocument( - PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) - throws IOException { - boolean imageIsLandscape = image.getWidth() > image.getHeight(); - PDRectangle pageSize = PDRectangle.A4; - - System.out.println(fitOption); - - if (autoRotate && imageIsLandscape) { - pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth()); - } - - if ("fitDocumentToImage".equals(fitOption)) { - pageSize = new PDRectangle(image.getWidth(), image.getHeight()); - } - - PDPage page = new PDPage(pageSize); - doc.addPage(page); - - float pageWidth = page.getMediaBox().getWidth(); - float pageHeight = page.getMediaBox().getHeight(); - - try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { - if ("fillPage".equals(fitOption) || "fitDocumentToImage".equals(fitOption)) { - contentStream.drawImage(image, 0, 0, pageWidth, pageHeight); - } else if ("maintainAspectRatio".equals(fitOption)) { - float imageAspectRatio = (float) image.getWidth() / (float) image.getHeight(); - float pageAspectRatio = pageWidth / pageHeight; - - float scaleFactor = 1.0f; - if (imageAspectRatio > pageAspectRatio) { - scaleFactor = pageWidth / image.getWidth(); - } else { - scaleFactor = pageHeight / image.getHeight(); - } - - float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; - float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; - contentStream.drawImage( - image, - xPos, - yPos, - image.getWidth() * scaleFactor, - image.getHeight() * scaleFactor); - } - } catch (IOException e) { - logger.error("Error adding image to PDF", e); - throw e; - } - } - - public static byte[] overlayImage( - byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) - throws IOException { - - PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes)); - - // Get the first page of the PDF - int pages = document.getNumberOfPages(); - for (int i = 0; i < pages; i++) { - PDPage page = document.getPage(i); - try (PDPageContentStream contentStream = - new PDPageContentStream( - document, page, PDPageContentStream.AppendMode.APPEND, true)) { - // Create an image object from the image bytes - PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); - // Draw the image onto the page at the specified x and y coordinates - contentStream.drawImage(image, x, y); - logger.info("Image successfully overlayed onto PDF"); - if (!everyPage && i == 0) { - break; - } - } catch (IOException e) { - // Log an error message if there is an issue overlaying the image onto the PDF - logger.error("Error overlaying image onto PDF", e); - throw e; - } - } - // Create a ByteArrayOutputStream to save the PDF to - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - logger.info("PDF successfully saved to byte array"); - return baos.toByteArray(); - } -} +package stirling.software.SPDF.utils; + +import java.awt.Graphics; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageOutputStream; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.text.PDFTextStripper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.multipart.MultipartFile; + +import stirling.software.SPDF.pdf.ImageFinder; + +public class PdfUtils { + + private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); + + public static PDRectangle textToPageSize(String size) { + switch (size.toUpperCase()) { + case "A0": + return PDRectangle.A0; + case "A1": + return PDRectangle.A1; + case "A2": + return PDRectangle.A2; + case "A3": + return PDRectangle.A3; + case "A4": + return PDRectangle.A4; + case "A5": + return PDRectangle.A5; + case "A6": + return PDRectangle.A6; + case "LETTER": + return PDRectangle.LETTER; + case "LEGAL": + return PDRectangle.LEGAL; + default: + throw new IllegalArgumentException("Invalid standard page size: " + size); + } + } + + public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { + String[] pageOrderArr = pagesToCheck.split(","); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + + for (int pageNumber : pageList) { + PDPage page = document.getPage(pageNumber); + if (hasImagesOnPage(page)) { + return true; + } + } + + return false; + } + + public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) + throws IOException { + String[] pageOrderArr = pageNumbersToCheck.split(","); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + + for (int pageNumber : pageList) { + PDPage page = document.getPage(pageNumber); + if (hasTextOnPage(page, phrase)) { + return true; + } + } + + return false; + } + + public static boolean hasImagesOnPage(PDPage page) throws IOException { + ImageFinder imageFinder = new ImageFinder(page); + imageFinder.processPage(page); + return imageFinder.hasImages(); + } + + public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + PDDocument tempDoc = new PDDocument(); + tempDoc.addPage(page); + String pageText = textStripper.getText(tempDoc); + tempDoc.close(); + return pageText.contains(phrase); + } + + public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) + throws IOException { + PDFTextStripper textStripper = new PDFTextStripper(); + String pdfText = ""; + + if (pagesToCheck == null || pagesToCheck.equals("all")) { + 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, + ImageType colorType, + boolean singleImage, + int DPI, + String filename) + throws IOException, Exception { + try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { + PDFRenderer pdfRenderer = new PDFRenderer(document); + int pageCount = document.getNumberOfPages(); + + // Create a ByteArrayOutputStream to save the image(s) to + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + if (singleImage) { + if (imageType.toLowerCase().equals("tiff") + || imageType.toLowerCase().equals("tif")) { + // Write the images to the output stream as a TIFF with multiple frames + ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next(); + ImageWriteParam param = writer.getDefaultWriteParam(); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionType("ZLib"); + param.setCompressionQuality(1.0f); + + try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) { + writer.setOutput(ios); + writer.prepareWriteSequence(null); + + for (int i = 0; i < pageCount; ++i) { + BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); + writer.writeToSequence(new IIOImage(image, null, null), param); + } + + writer.endWriteSequence(); + } + + writer.dispose(); + } else { + // Combine all images into a single big image + BufferedImage image = pdfRenderer.renderImageWithDPI(0, DPI, colorType); + BufferedImage combined = + new BufferedImage( + image.getWidth(), + image.getHeight() * pageCount, + BufferedImage.TYPE_INT_RGB); + Graphics g = combined.getGraphics(); + + for (int i = 0; i < pageCount; ++i) { + if (i != 0) { + image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); + } + g.drawImage(image, 0, i * image.getHeight(), null); + } + + // Write the image to the output stream + ImageIO.write(combined, imageType, baos); + } + + // Log that the image was successfully written to the byte array + logger.info("Image successfully written to byte array"); + } else { + // Zip the images and return as byte array + try (ZipOutputStream zos = new ZipOutputStream(baos)) { + for (int i = 0; i < pageCount; ++i) { + BufferedImage image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); + try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) { + ImageIO.write(image, imageType, baosImage); + + // Add the image to the zip file + zos.putNextEntry( + new ZipEntry( + String.format( + filename + "_%d.%s", + i + 1, + imageType.toLowerCase()))); + zos.write(baosImage.toByteArray()); + } + } + // Log that the images were successfully written to the byte array + logger.info("Images successfully written to byte array as a zip"); + } + } + return baos.toByteArray(); + } catch (IOException e) { + // Log an error message if there is an issue converting the PDF to an image + logger.error("Error converting PDF to image", e); + throw e; + } + } + + public static byte[] imageToPdf( + MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) + throws IOException { + try (PDDocument doc = new PDDocument()) { + for (MultipartFile file : files) { + String contentType = file.getContentType(); + String originalFilename = file.getOriginalFilename(); + if (originalFilename != null + && (originalFilename.toLowerCase().endsWith(".tiff") + || originalFilename.toLowerCase().endsWith(".tif"))) { + ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next(); + reader.setInput(ImageIO.createImageInputStream(file.getInputStream())); + int numPages = reader.getNumImages(true); + for (int i = 0; i < numPages; i++) { + BufferedImage pageImage = reader.read(i); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(pageImage, colorType); + PDImageXObject pdImage = + LosslessFactory.createFromImage(doc, convertedImage); + addImageToDocument(doc, pdImage, fitOption, autoRotate); + } + } else { + BufferedImage image = ImageIO.read(file.getInputStream()); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(image, colorType); + // Use JPEGFactory if it's JPEG since JPEG is lossy + PDImageXObject pdImage = + (contentType != null && contentType.equals("image/jpeg")) + ? JPEGFactory.createFromImage(doc, convertedImage) + : LosslessFactory.createFromImage(doc, convertedImage); + addImageToDocument(doc, pdImage, fitOption, autoRotate); + } + } + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + doc.save(byteArrayOutputStream); + logger.info("PDF successfully saved to byte array"); + return byteArrayOutputStream.toByteArray(); + } + } + + private static void addImageToDocument( + PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) + throws IOException { + boolean imageIsLandscape = image.getWidth() > image.getHeight(); + PDRectangle pageSize = PDRectangle.A4; + + System.out.println(fitOption); + + if (autoRotate && imageIsLandscape) { + pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth()); + } + + if ("fitDocumentToImage".equals(fitOption)) { + pageSize = new PDRectangle(image.getWidth(), image.getHeight()); + } + + PDPage page = new PDPage(pageSize); + doc.addPage(page); + + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + + try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) { + if ("fillPage".equals(fitOption) || "fitDocumentToImage".equals(fitOption)) { + contentStream.drawImage(image, 0, 0, pageWidth, pageHeight); + } else if ("maintainAspectRatio".equals(fitOption)) { + float imageAspectRatio = (float) image.getWidth() / (float) image.getHeight(); + float pageAspectRatio = pageWidth / pageHeight; + + float scaleFactor = 1.0f; + if (imageAspectRatio > pageAspectRatio) { + scaleFactor = pageWidth / image.getWidth(); + } else { + scaleFactor = pageHeight / image.getHeight(); + } + + float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; + float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; + contentStream.drawImage( + image, + xPos, + yPos, + image.getWidth() * scaleFactor, + image.getHeight() * scaleFactor); + } + } catch (IOException e) { + logger.error("Error adding image to PDF", e); + throw e; + } + } + + public static byte[] overlayImage( + byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) + throws IOException { + + PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes)); + + // Get the first page of the PDF + int pages = document.getNumberOfPages(); + for (int i = 0; i < pages; i++) { + PDPage page = document.getPage(i); + try (PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true)) { + // Create an image object from the image bytes + PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); + // Draw the image onto the page at the specified x and y coordinates + contentStream.drawImage(image, x, y); + logger.info("Image successfully overlayed onto PDF"); + if (!everyPage && i == 0) { + break; + } + } catch (IOException e) { + // Log an error message if there is an issue overlaying the image onto the PDF + logger.error("Error overlaying image onto PDF", e); + throw e; + } + } + // Create a ByteArrayOutputStream to save the PDF to + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + logger.info("PDF successfully saved to byte array"); + return baos.toByteArray(); + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java index 4958a00d..1114de64 100644 --- a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java @@ -1,67 +1,67 @@ -package stirling.software.SPDF.utils; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; - -import org.apache.pdfbox.pdmodel.PDDocument; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.multipart.MultipartFile; - -public class WebResponseUtils { - - public static ResponseEntity boasToWebResponse( - ByteArrayOutputStream baos, String docName) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); - } - - public static ResponseEntity boasToWebResponse( - ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); - } - - public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) - throws IOException { - String fileName = file.getOriginalFilename(); - MediaType mediaType = MediaType.parseMediaType(file.getContentType()); - - byte[] bytes = file.getBytes(); - - return bytesToWebResponse(bytes, fileName, mediaType); - } - - public static ResponseEntity bytesToWebResponse( - byte[] bytes, String docName, MediaType mediaType) throws IOException { - - // Return the PDF as a response - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(mediaType); - headers.setContentLength(bytes.length); - String encodedDocName = - URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) - .replaceAll("\\+", "%20"); - headers.setContentDispositionFormData("attachment", encodedDocName); - return new ResponseEntity<>(bytes, headers, HttpStatus.OK); - } - - public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) - throws IOException { - return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); - } - - public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) - throws IOException { - - // Open Byte Array and save document to it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - // Close the document - document.close(); - - return boasToWebResponse(baos, docName); - } -} +package stirling.software.SPDF.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; + +public class WebResponseUtils { + + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); + } + + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); + } + + public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) + throws IOException { + String fileName = file.getOriginalFilename(); + MediaType mediaType = MediaType.parseMediaType(file.getContentType()); + + byte[] bytes = file.getBytes(); + + return bytesToWebResponse(bytes, fileName, mediaType); + } + + public static ResponseEntity bytesToWebResponse( + byte[] bytes, String docName, MediaType mediaType) throws IOException { + + // Return the PDF as a response + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(bytes.length); + String encodedDocName = + URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + headers.setContentDispositionFormData("attachment", encodedDocName); + return new ResponseEntity<>(bytes, headers, HttpStatus.OK); + } + + public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) + throws IOException { + return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); + } + + public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) + throws IOException { + + // Open Byte Array and save document to it + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + // Close the document + document.close(); + + return boasToWebResponse(baos, docName); + } +}