diff --git a/.gitignore b/.gitignore index 67a9a4b5..b0c87c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ local.properties .classpath .project version.properties -pipeline/ - +pipeline/watchedFolders/ +pipeline/finishedFolders/ #### Stirling-PDF Files ### customFiles/ configs/ diff --git a/Dockerfile b/Dockerfile index 6d830346..f0eb592c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ ARG VERSION_TAG # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -18,13 +19,14 @@ ENV DOCKER_ENABLE_SECURITY=false \ ## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # Set up necessary directories and permissions -RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles +RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders ##&& \ ## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original # Copy necessary files COPY ./scripts/* /scripts/ +COPY ./pipeline/ /pipeline/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY build/libs/*.jar app.jar @@ -42,4 +44,4 @@ EXPOSE 8080 # Set user and run command ##USER stirlingpdfuser ENTRYPOINT ["/scripts/init.sh"] -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/Dockerfile-lite b/Dockerfile-lite index 9d734942..054c7639 100644 --- a/Dockerfile-lite +++ b/Dockerfile-lite @@ -17,7 +17,8 @@ RUN apt-get update && \ # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -28,13 +29,14 @@ ENV DOCKER_ENABLE_SECURITY=false \ # mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # Set up necessary directories and permissions -RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles +RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders # chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles # Copy necessary files COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh +COPY ./pipeline/ /pipeline/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY build/libs/*.jar app.jar @@ -60,4 +62,4 @@ ENV DOCKER_ENABLE_SECURITY=false # Run the application #USER stirlingpdfuser ENTRYPOINT ["/scripts/init-without-ocr.sh"] -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/Dockerfile-ultra-lite b/Dockerfile-ultra-lite index b49b9023..d77c1b94 100644 --- a/Dockerfile-ultra-lite +++ b/Dockerfile-ultra-lite @@ -6,7 +6,8 @@ ARG VERSION_TAG # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -18,12 +19,12 @@ ENV DOCKER_ENABLE_SECURITY=false \ # Set up necessary directories and permissions #RUN mkdir -p /scripts /configs /customFiles && \ -# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles - +# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders + RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh - +COPY ./pipeline/ /pipeline/ COPY build/libs/*.jar app.jar # Set font cache and permissions @@ -42,4 +43,4 @@ ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI ENTRYPOINT ["/scripts/init-without-ocr.sh"] # Run the application -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/README.md b/README.md index 5a37bb30..d9515ca6 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md ## Want to add your own language? -Stirling PDF currently supports 20! +Stirling PDF currently supports 21! - English (English) (en_GB) - English (US) (en_US) - Arabic (العربية) (ar_AR) @@ -169,6 +169,7 @@ Stirling PDF currently supports 20! - Greek (el_GR) - Turkish (Türkçe) (tr_TR) - Indonesia (Bahasa Indonesia) (id_ID) +- Hindi (हिंदी) (hi_IN) If you want to add your own language to Stirling-PDF please refer https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md diff --git a/build.gradle b/build.gradle index 66de762e..8002ee7a 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'stirling.software' -version = '0.17.2' +version = '0.18.1' sourceCompatibility = '17' repositories { @@ -48,7 +48,7 @@ launch4j { errTitle="Encountered error, Do you have Java 17?" downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe" - variables=["BROWSER_OPEN=true"] + variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"] jreMinVersion="17" mutexName="Stirling-PDF" @@ -68,17 +68,17 @@ dependencies { implementation 'org.springframework:spring-webmvc:6.0.15' implementation 'org.yaml:snakeyaml:2.1' - implementation 'org.springframework.boot:spring-boot-starter-web:3.1.6' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.6' + implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1' if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { - implementation 'org.springframework.boot:spring-boot-starter-security:3.1.6' + implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' implementation "org.springframework.boot:spring-boot-starter-data-jpa" implementation "com.h2database:h2" } - testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.6' + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1' // Batik implementation 'org.apache.xmlgraphics:batik-all:1.17' diff --git a/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json b/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json new file mode 100644 index 00000000..ba3228dc --- /dev/null +++ b/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json @@ -0,0 +1,39 @@ +{ + "name": "Prepare-pdfs-for-email", + "pipeline": [ + { + "operation": "/api/v1/misc/repair", + "parameters": {} + }, + { + "operation": "/api/v1/security/sanitize-pdf", + "parameters": { + "removeJavaScript": true, + "removeEmbeddedFiles": false, + "removeMetadata": false, + "removeLinks": false, + "removeFonts": false + } + }, + { + "operation": "/api/v1/misc/compress-pdf", + "parameters": { + "optimizeLevel": 2, + "expectedOutputSize": "" + } + }, + { + "operation": "/api/v1/general/split-by-size-or-count", + "parameters": { + "splitType": 0, + "splitValue": "15MB" + } + } + ], + "_examples": { + "outputDir": "{outputFolder}/{folderName}", + "outputFileName": "{filename}-{pipelineName}-{date}-{time}" + }, + "outputDir": "httpWebRequest", + "outputFileName": "{filename}" +} \ No newline at end of file diff --git a/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json b/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json new file mode 100644 index 00000000..3a989296 --- /dev/null +++ b/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json @@ -0,0 +1,33 @@ +{ + "name": "split-rotate-auto-rename", + "pipeline": [ + { + "operation": "/api/v1/general/split-pdf-by-sections", + "parameters": { + "horizontalDivisions": 2, + "verticalDivisions": 2, + "fileInput": "automated" + } + }, + { + "operation": "/api/v1/general/rotate-pdf", + "parameters": { + "angle": 90, + "fileInput": "automated" + } + }, + { + "operation": "/api/v1/misc/auto-rename", + "parameters": { + "useFirstTextAsFallback": false, + "fileInput": "automated" + } + } + ], + "_examples": { + "outputDir": "{outputFolder}/{folderName}", + "outputFileName": "{filename}-{pipelineName}-{date}-{time}" + }, + "outputDir": "{outputFolder}", + "outputFileName": "{filename}" +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 5dd7fed3..a7ce7f14 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -8,15 +8,16 @@ 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 +@EnableScheduling public class SPdfApplication { - + @Autowired private Environment env; @@ -28,11 +29,7 @@ public class SPdfApplication { if (browserOpen) { try { - String port = env.getProperty("local.server.port"); - if(port == null || port.length() == 0) { - port="8080"; - } - String url = "http://localhost:" + port; + String url = "http://localhost:" + getPort(); String os = System.getProperty("os.name").toLowerCase(); Runtime rt = Runtime.getRuntime(); @@ -45,7 +42,7 @@ public class SPdfApplication { } } } - + public static void main(String[] args) { SpringApplication app = new SpringApplication(SPdfApplication.class); app.addInitializers(new ConfigInitializer()); @@ -55,28 +52,28 @@ public class SPdfApplication { 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 port = System.getProperty("local.server.port"); - if(port == null || port.length() == 0) { - port="8080"; - } - String url = "http://localhost:" + port; + + String url = "http://localhost:" + getPort(); System.out.println("Navigate to " + url); } - - -} \ No newline at end of file + + 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 4d02f974..faf85b28 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -41,12 +41,16 @@ public class AppConfig { 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"); - System.out.println("rateLimit=" + appName); return (appName != null) ? Boolean.valueOf(appName) : false; } diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 6ee59db7..9abb68bf 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -16,33 +16,35 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class MetricsFilter extends OncePerRequestFilter { - private final MeterRegistry meterRegistry; + private final MeterRegistry meterRegistry; - @Autowired - public MetricsFilter(MeterRegistry meterRegistry) { - this.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("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { - Counter counter = Counter.builder("http.requests") - .tag("uri", uri) - .tag("method", request.getMethod()) - .register(meterRegistry); + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); - counter.increment(); - //System.out.println("Counted"); - } + // 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); - filterChain.doFilter(request, response); - } + 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 2583277e..5dba40d0 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,27 +1,42 @@ 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 - - } + 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")); + } - 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.")); } - -} +} \ No newline at end of file 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 f286f149..397a8a70 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -2,27 +2,46 @@ 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); - if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { - setDefaultFailureUrl("/login?error=badcredentials"); - } else if (exception.getClass().isAssignableFrom(LockedException.class)) { + + 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/CustomAuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java new file mode 100644 index 00000000..cd2217e1 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,44 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +import org.springframework.security.web.savedrequest.SavedRequest; +import org.springframework.stereotype.Component; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import stirling.software.SPDF.utils.RequestUriUtils; + +@Component +public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { + + @Autowired + private LoginAttemptService loginAttemptService; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { + String username = request.getParameter("username"); + loginAttemptService.loginSucceeded(username); + + + // Get the saved request + HttpSession session = request.getSession(false); + SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null; + if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { + // Redirect to the original destination + super.onAuthenticationSuccess(request, response, authentication); + } else { + // Redirect to the root URL (considering context path) + getRedirectStrategy().sendRedirect(request, response, "/"); + } + + //super.onAuthenticationSuccess(request, response, authentication); + } + + +} 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 7ae1680b..77db2cd4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -5,6 +5,7 @@ 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; @@ -22,12 +23,18 @@ 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(), diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java index 8e612464..65acf148 100644 --- a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -15,6 +15,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.User; +import stirling.software.SPDF.utils.RequestUriUtils; @Component public class FirstLoginFilter extends OncePerRequestFilter { @@ -28,11 +29,7 @@ public class FirstLoginFilter extends OncePerRequestFilter { String method = request.getMethod(); String requestURI = request.getRequestURI(); // Check if the request is for static resources - boolean isStaticResource = requestURI.startsWith("/css/") - || requestURI.startsWith("/js/") - || requestURI.startsWith("/images/") - || requestURI.startsWith("/public/") - || requestURI.endsWith(".svg"); + boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); // If it's a static resource, just continue the filter chain and skip the logic below if (isStaticResource) { diff --git a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java new file mode 100644 index 00000000..03e34b57 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java @@ -0,0 +1,65 @@ +package stirling.software.SPDF.config.security; +import java.io.IOException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import stirling.software.SPDF.utils.RequestUriUtils; + +public class IPRateLimitingFilter implements Filter { + + private final ConcurrentHashMap requestCounts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap getCounts = new ConcurrentHashMap<>(); + private final int maxRequests; + private final int maxGetRequests; + + public IPRateLimitingFilter(int maxRequests, int maxGetRequests) { + this.maxRequests = maxRequests; + this.maxGetRequests = maxGetRequests; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + HttpServletRequest httpRequest = (HttpServletRequest) request; + String method = httpRequest.getMethod(); + String requestURI = httpRequest.getRequestURI(); + // Check if the request is for static resources + boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI); + + // If it's a static resource, just continue the filter chain and skip the logic below + if (isStaticResource) { + chain.doFilter(request, response); + return; + } + + String clientIp = request.getRemoteAddr(); + requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0)); + if (!"GET".equalsIgnoreCase(method)) { + + if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) { + // Handle limit exceeded (e.g., send error response) + response.getWriter().write("Rate limit exceeded"); + return; + } + } else { + if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) { + // Handle limit exceeded (e.g., send error response) + response.getWriter().write("GET Rate limit exceeded"); + return; + } + } + } + chain.doFilter(request, response); + } + + public void resetRequestCounts() { + requestCounts.clear(); + getCounts.clear(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 842375d8..5d100dd8 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -37,8 +37,10 @@ public class InitialSecuritySetup { initialPassword = "stirling"; userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); } - - + } + if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) { + userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId()); + userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java new file mode 100644 index 00000000..55a84449 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java @@ -0,0 +1,56 @@ +package stirling.software.SPDF.config.security; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import jakarta.annotation.PostConstruct; +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.AttemptCounter; + +@Service +public class LoginAttemptService { + + + @Autowired + ApplicationProperties applicationProperties; + + private int MAX_ATTEMPTS; + private long ATTEMPT_INCREMENT_TIME; + + + @PostConstruct + public void init() { + MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount(); + ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes()); + } + + private final ConcurrentHashMap attemptsCache = new ConcurrentHashMap<>(); + + public void loginSucceeded(String key) { + attemptsCache.remove(key); + } + + public boolean loginAttemptCheck(String key) { + attemptsCache.compute(key, (k, attemptCounter) -> { + if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { + return new AttemptCounter(); + } else { + attemptCounter.increment(); + return attemptCounter; + } + }); + return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS; + } + + + public boolean isBlocked(String key) { + AttemptCounter attemptCounter = attemptsCache.get(key); + if (attemptCounter != null) { + return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS; + } + return false; + } + +} diff --git a/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java new file mode 100644 index 00000000..3ef8ef31 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java @@ -0,0 +1,18 @@ +package stirling.software.SPDF.config.security; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class RateLimitResetScheduler { + + private final IPRateLimitingFilter rateLimitingFilter; + + public RateLimitResetScheduler(IPRateLimitingFilter rateLimitingFilter) { + this.rateLimitingFilter = rateLimitingFilter; + } + + @Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable + public void resetRateLimit() { + rateLimitingFilter.resetRequestCounts(); + } +} 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 dfe782ac..e0b439db 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -6,7 +6,7 @@ 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.EnableGlobalMethodSecurity; +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; @@ -15,12 +15,13 @@ 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() -@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity public class SecurityConfiguration { @Autowired @@ -40,6 +41,11 @@ public class SecurityConfiguration { @Autowired private UserAuthenticationFilter userAuthenticationFilter; + + + + @Autowired + private LoginAttemptService loginAttemptService; @Autowired private FirstLoginFilter firstLoginFilter; @@ -51,14 +57,18 @@ public class SecurityConfiguration { 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()) + .failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService)) .permitAll() - ) + ).requestCache(requestCache -> requestCache + .requestCache(new NullRequestCache()) + ) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/login?logout=true") @@ -70,8 +80,19 @@ public class SecurityConfiguration { .tokenValiditySeconds(1209600) // 2 weeks ) .authorizeHttpRequests(authz -> authz - .requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/")) - .permitAll() + .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/"); + } + ).permitAll() .anyRequest().authenticated() ) .userDetailsService(userDetailsService) @@ -84,8 +105,17 @@ public class SecurityConfiguration { } 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() { 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 eca7f70e..ce77e5a4 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -74,15 +74,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // If we still don't have any authentication, deny the request if (authentication == null || !authentication.isAuthenticated()) { String method = request.getMethod(); - if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) { - response.sendRedirect("/login"); // redirect to the login page + 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); } @@ -90,15 +92,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String uri = request.getRequestURI(); - + String contextPath = request.getContextPath(); String[] permitAllPatterns = { - "/login", - "/register", - "/error", - "/images/", - "/public/", - "/css/", - "/js/" + contextPath + "/login", + contextPath + "/register", + contextPath + "/error", + contextPath + "/images/", + contextPath + "/public/", + contextPath + "/css/", + contextPath + "/js/", + contextPath + "/pdfjs/", + contextPath + "/site.webmanifest" }; for (String pattern : permitAllPatterns) { 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 46c5aeff..45794d92 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -16,11 +16,13 @@ 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 { +public class UserService implements UserServiceInterface{ @Autowired private UserRepository userRepository; @@ -136,6 +138,11 @@ public class UserService { 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()); } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 1dc81e9f..9551754a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -3,8 +3,9 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.HashMap; +import java.util.List; import java.util.Map; - +import java.util.ArrayList; import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.pdmodel.PDDocument; import org.springframework.http.MediaType; @@ -25,7 +26,7 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class PdfOverlayController { - @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") + @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") @Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { MultipartFile baseFile = request.getFileInput(); @@ -33,44 +34,53 @@ public class PdfOverlayController { MultipartFile[] overlayFiles = request.getOverlayFiles(); File[] overlayPdfFiles = new File[overlayFiles.length]; - try{ - for (int i = 0; i < overlayFiles.length; i++) { - overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); - } - - String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" - int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode - - try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); - Overlay overlay = new Overlay()) { - Map overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts); - - overlay.setInputPDF(basePdf); - if(overlayPos == 0) { - overlay.setOverlayPosition(Overlay.Position.FOREGROUND); - } else { - overlay.setOverlayPosition(Overlay.Position.BACKGROUND); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - overlay.overlay(overlayGuide).save(outputStream); - byte[] data = outputStream.toByteArray(); - String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf - - return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); - } + List tempFiles = new ArrayList<>(); // List to keep track of temporary files + + try { + for (int i = 0; i < overlayFiles.length; i++) { + overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); + } + + String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" + int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode + + try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); + Overlay overlay = new Overlay()) { + Map overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles); + + overlay.setInputPDF(basePdf); + if (overlayPos == 0) { + overlay.setOverlayPosition(Overlay.Position.FOREGROUND); + } else { + overlay.setOverlayPosition(Overlay.Position.BACKGROUND); + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + overlay.overlay(overlayGuide).save(outputStream); + byte[] data = outputStream.toByteArray(); + String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf + + return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); + } } finally { for (File overlayPdfFile : overlayPdfFiles) { - if (overlayPdfFile != null) overlayPdfFile.delete(); + if (overlayPdfFile != null) { + overlayPdfFile.delete(); + } + } + for (File tempFile : tempFiles) { // Delete temporary files + if (tempFile != null) { + tempFile.delete(); + } } } } - private Map prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts) throws IOException { + private Map prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) throws IOException { Map overlayGuide = new HashMap<>(); switch (mode) { case "SequentialOverlay": - sequentialOverlay(overlayGuide, overlayFiles, basePageCount); + sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles); break; case "InterleavedOverlay": interleavedOverlay(overlayGuide, overlayFiles, basePageCount); @@ -84,42 +94,80 @@ public class PdfOverlayController { return overlayGuide; } - private void sequentialOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { - if (overlayFiles.length != 1 || basePageCount != PDDocument.load(overlayFiles[0]).getNumberOfPages()) { - throw new IllegalArgumentException("Overlay file count and base page count must match for sequential overlay."); - } + private void sequentialOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount, List tempFiles) throws IOException { + int overlayFileIndex = 0; + int pageCountInCurrentOverlay = 0; - File overlayFile = overlayFiles[0]; - try (PDDocument overlayPdf = PDDocument.load(overlayFile)) { - for (int i = 1; i <= overlayPdf.getNumberOfPages(); i++) { - if (i > basePageCount) break; - overlayGuide.put(i, overlayFile.getAbsolutePath()); + for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { + if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) { + pageCountInCurrentOverlay = 0; + overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; + } + + try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) { + PDDocument singlePageDocument = new PDDocument(); + singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay)); + File tempFile = File.createTempFile("overlay-page-", ".pdf"); + singlePageDocument.save(tempFile); + singlePageDocument.close(); + + overlayGuide.put(basePageIndex, tempFile.getAbsolutePath()); + tempFiles.add(tempFile); // Keep track of the temporary file for cleanup + } + + pageCountInCurrentOverlay++; + } + } + + private int getNumberOfPages(File file) throws IOException { + try (PDDocument doc = PDDocument.load(file)) { + return doc.getNumberOfPages(); + } + } + + + + + + + + private void interleavedOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { + for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { + File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length]; + + // Load the overlay document to check its page count + try (PDDocument overlayPdf = PDDocument.load(overlayFile)) { + int overlayPageCount = overlayPdf.getNumberOfPages(); + if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) { + overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath()); + } } } } - private void interleavedOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { - for (int i = 0; i < basePageCount; i++) { - File overlayFile = overlayFiles[i % overlayFiles.length]; - overlayGuide.put(i + 1, overlayFile.getAbsolutePath()); - } - } - private void fixedRepeatOverlay(Map overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { if (overlayFiles.length != counts.length) { throw new IllegalArgumentException("Counts array length must match the number of overlay files"); } int currentPage = 1; for (int i = 0; i < overlayFiles.length; i++) { - File overlayFile = overlayFiles[i]; + File overlayFile = overlayFiles[i]; int repeatCount = counts[i]; - for (int j = 0; j < repeatCount; j++) { - if (currentPage > basePageCount) break; - overlayGuide.put(currentPage++, overlayFile.getAbsolutePath()); + + // Load the overlay document to check its page count + try (PDDocument overlayPdf = PDDocument.load(overlayFile)) { + int overlayPageCount = overlayPdf.getNumberOfPages(); + for (int j = 0; j < repeatCount; j++) { + for (int page = 0; page < overlayPageCount; page++) { + if (currentPage > basePageCount) break; + overlayGuide.put(currentPage++, overlayFile.getAbsolutePath()); + } + } } } } + } // Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere. 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 29a2f426..f36f64da 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -29,12 +29,12 @@ import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @RequestMapping("/api/v1/general") -@Tag(name = "Misc", description = "Miscellaneous APIs") +@Tag(name = "General", description = "General APIs") public class SplitPdfBySectionsController { @PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") - @Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input: PDF, Split Parameters. Output: ZIP containing split documents.") + @Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO") public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { List splitDocumentsBoas = new ArrayList<>(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index 83e7b723..9b25e8be 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -26,13 +26,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RestController @RequestMapping("/api/v1/general") -@Tag(name = "Misc", description = "Miscellaneous APIs") +@Tag(name = "General", description = "General APIs") public class SplitPdfBySizeController { @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") @Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" - + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP Type:SIMO") + + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO") public ResponseEntity autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { List splitDocumentsBoas = new ArrayList(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index bf451567..01a50a3b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -23,6 +23,7 @@ import org.springframework.web.servlet.view.RedirectView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; @Controller @@ -32,6 +33,7 @@ public class UserController { @Autowired private UserService userService; + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") public String register(@RequestParam String username, @RequestParam String password, Model model) { if(userService.usernameExists(username)) { @@ -43,6 +45,7 @@ public class UserController { return "redirect:/login?registered=true"; } + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username-and-password") public RedirectView changeUsernameAndPassword(Principal principal, @RequestParam String currentPassword, @@ -85,7 +88,7 @@ public class UserController { } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username") public RedirectView changeUsername(Principal principal, @RequestParam String currentPassword, @@ -122,7 +125,8 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-password") public RedirectView changePassword(Principal principal, @RequestParam String currentPassword, @@ -154,7 +158,7 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/updateUserSettings") public String updateUserSettings(HttpServletRequest request, Principal principal) { Map paramMap = request.getParameterMap(); @@ -182,6 +186,18 @@ public class UserController { if(userService.usernameExists(username)) { return new RedirectView("/addUsers?messageType=usernameExists"); } + try { + // Validate the role + Role roleEnum = Role.fromString(role); + if (roleEnum == Role.INTERNAL_API_USER) { + // If the role is INTERNAL_API_USER, reject the request + return new RedirectView("/addUsers?messageType=invalidRole"); + } + } catch (IllegalArgumentException e) { + // If the role ID is not valid, redirect with an error message + return new RedirectView("/addUsers?messageType=invalidRole"); + } + userService.saveUser(username, password, role, forceChange); return new RedirectView("/addUsers"); // Redirect to account page after adding the user } @@ -203,6 +219,7 @@ public class UserController { return "redirect:/addUsers"; } + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/get-api-key") public ResponseEntity getApiKey(Principal principal) { if (principal == null) { @@ -216,6 +233,7 @@ public class UserController { return ResponseEntity.ok(apiKey); } + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/update-api-key") public ResponseEntity updateApiKey(Principal principal) { if (principal == null) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java deleted file mode 100644 index e821a36a..00000000 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java +++ /dev/null @@ -1,130 +0,0 @@ -package stirling.software.SPDF.controller.api.converters; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -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 org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import io.swagger.v3.oas.annotations.Hidden; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import stirling.software.SPDF.model.api.GeneralFile; -import stirling.software.SPDF.utils.FileToPdf; -import stirling.software.SPDF.utils.WebResponseUtils; - -@RestController -@RequestMapping("/api/v1/convert") -@Tag(name = "Convert", description = "Convert APIs") -public class ConvertEpubToPdf { - //TODO - @PostMapping(consumes = "multipart/form-data", value = "/epub-to-single-pdf") - @Hidden - @Operation( - summary = "Convert an EPUB file to a single PDF", - description = "This endpoint takes an EPUB file input and converts it to a single PDF." - ) - public ResponseEntity epubToSinglePdf( - @ModelAttribute GeneralFile request) - throws Exception { - MultipartFile fileInput = request.getFileInput(); - if (fileInput == null) { - throw new IllegalArgumentException("Please provide an EPUB file for conversion."); - } - - String originalFilename = fileInput.getOriginalFilename(); - if (originalFilename == null || !originalFilename.endsWith(".epub")) { - throw new IllegalArgumentException("File must be in .epub format."); - } - - Map epubContents = extractEpubContent(fileInput); - List htmlFilesOrder = getHtmlFilesOrderFromOpf(epubContents); - - List individualPdfs = new ArrayList<>(); - - for (String htmlFile : htmlFilesOrder) { - byte[] htmlContent = epubContents.get(htmlFile); - byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent, htmlFile.replace(".html", ".pdf")); - individualPdfs.add(pdfBytes); - } - - // Pseudo-code to merge individual PDFs into one. - byte[] mergedPdfBytes = mergeMultiplePdfsIntoOne(individualPdfs); - - return WebResponseUtils.bytesToWebResponse(mergedPdfBytes, originalFilename.replace(".epub", ".pdf")); - } - - // Assuming a pseudo-code function that merges multiple PDFs into one. - private byte[] mergeMultiplePdfsIntoOne(List individualPdfs) { - // You can use a library such as PDFBox to perform the merging here. - // Return the byte[] of the merged PDF. - return null; - } - - private Map extractEpubContent(MultipartFile fileInput) throws IOException { - Map contentMap = new HashMap<>(); - - try (ZipInputStream zis = new ZipInputStream(fileInput.getInputStream())) { - ZipEntry zipEntry = zis.getNextEntry(); - while (zipEntry != null) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int read = 0; - while ((read = zis.read(buffer)) != -1) { - baos.write(buffer, 0, read); - } - contentMap.put(zipEntry.getName(), baos.toByteArray()); - zipEntry = zis.getNextEntry(); - } - } - - return contentMap; - } - - private List getHtmlFilesOrderFromOpf(Map epubContents) throws Exception { - String opfContent = new String(epubContents.get("OEBPS/content.opf")); // Adjusting for given path - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); - InputSource is = new InputSource(new StringReader(opfContent)); - Document doc = dBuilder.parse(is); - - NodeList itemRefs = doc.getElementsByTagName("itemref"); - List htmlFilesOrder = new ArrayList<>(); - - for (int i = 0; i < itemRefs.getLength(); i++) { - Element itemRef = (Element) itemRefs.item(i); - String idref = itemRef.getAttribute("idref"); - - NodeList items = doc.getElementsByTagName("item"); - for (int j = 0; j < items.getLength(); j++) { - Element item = (Element) items.item(j); - if (idref.equals(item.getAttribute("id"))) { - htmlFilesOrder.add(item.getAttribute("href")); // Fetching the actual href - break; - } - } - } - - return htmlFilesOrder; - } - - -} 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 7c81edf2..d3f5c307 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 @@ -28,7 +28,7 @@ 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." + 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(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java index f6fb69c9..6398e8b9 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java @@ -28,12 +28,12 @@ import stirling.software.SPDF.model.api.extract.PDFFilePage; @RestController @RequestMapping("/api/v1/convert") -@Tag(name = "General", description = "General APIs") +@Tag(name = "Convert", description = "Convert APIs") public class ExtractController { private static final Logger logger = LoggerFactory.getLogger(CropController.class); - @PostMapping(value = "/pdf-to-csv", consumes = "multipart/form-data") + @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") @Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") public ResponseEntity PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception { diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index b4b9b951..e62fc35f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -43,7 +43,7 @@ public class AutoSplitPdfController { private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") - @Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP Type:SISO") + @Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO") public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = request.isDuplexMode(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 27431346..cef32ac7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -15,6 +15,7 @@ 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.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; @@ -25,6 +26,7 @@ public class ShowJavascript { private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); @PostMapping(consumes = "multipart/form-data", value = "/show-javascript") + @Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO") public ResponseEntity extractHeader(@ModelAttribute PDFFile request) throws Exception { MultipartFile inputFile = request.getFileInput(); String script = ""; diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java new file mode 100644 index 00000000..c3bf9c8d --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java @@ -0,0 +1,123 @@ +package stirling.software.SPDF.controller.api.pipeline; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletContext; + +import stirling.software.SPDF.SPdfApplication; +import stirling.software.SPDF.model.ApiEndpoint; +import stirling.software.SPDF.model.Role; +import org.slf4j.Logger; +@Service +public class ApiDocService { + + private final Map apiDocumentation = new HashMap<>(); + + private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class); + + @Autowired + private ServletContext servletContext; + + private String getApiDocsUrl() { + String contextPath = servletContext.getContextPath(); + String port = SPdfApplication.getPort(); + + return "http://localhost:"+ port + contextPath + "/v1/api-docs"; + } + + + @Autowired(required=false) + private UserServiceInterface userService; + + private String getApiKeyForUser() { + if(userService == null) + return ""; + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } + + JsonNode apiDocsJsonRootNode; + + + //@EventListener(ApplicationReadyEvent.class) + private synchronized void loadApiDocumentation() { + String apiDocsJson = ""; + try { + HttpHeaders headers = new HttpHeaders(); + String apiKey = getApiKeyForUser(); + if (!apiKey.isEmpty()) { + headers.set("X-API-KEY", apiKey); + } + HttpEntity entity = new HttpEntity<>(headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); + apiDocsJson = response.getBody(); + + ObjectMapper mapper = new ObjectMapper(); + apiDocsJsonRootNode = mapper.readTree(apiDocsJson); + + JsonNode paths = apiDocsJsonRootNode.path("paths"); + paths.fields().forEachRemaining(entry -> { + String path = entry.getKey(); + JsonNode pathNode = entry.getValue(); + if (pathNode.has("post")) { + JsonNode postNode = pathNode.get("post"); + ApiEndpoint endpoint = new ApiEndpoint(path, postNode); + apiDocumentation.put(path, endpoint); + } + }); + } catch (Exception e) { + // Handle exceptions + logger.error("Error grabbing swagger doc, body result {}", apiDocsJson); + } + } + + public boolean isValidOperation(String operationName, Map parameters) { + if(apiDocumentation.size() == 0) { + loadApiDocumentation(); + } + if (!apiDocumentation.containsKey(operationName)) { + return false; + } + ApiEndpoint endpoint = apiDocumentation.get(operationName); + return endpoint.areParametersValid(parameters); + } + + public boolean isMultiInput(String operationName) { + if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { + loadApiDocumentation(); + } + if (!apiDocumentation.containsKey(operationName)) { + return false; + } + + ApiEndpoint endpoint = apiDocumentation.get(operationName); + String description = endpoint.getDescription(); + + Pattern pattern = Pattern.compile("Type:(\\w+)"); + Matcher matcher = pattern.matcher(description); + if (matcher.find()) { + String type = matcher.group(1); + return type.startsWith("MI"); + } + + return false; + } +} + +// Model class for API Endpoint + diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index c12fe724..a6f78a6f 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,56 +1,31 @@ package stirling.software.SPDF.controller.api.pipeline; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Iterator; import java.util.List; -import java.util.Map; -import java.util.stream.Stream; import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; 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.ByteArrayResource; import org.springframework.core.io.Resource; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; 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.client.RestTemplate; import org.springframework.web.multipart.MultipartFile; -import com.fasterxml.jackson.databind.JsonNode; +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.PipelineOperation; import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -60,373 +35,39 @@ import stirling.software.SPDF.utils.WebResponseUtils; public class PipelineController { private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); - @Autowired - private ObjectMapper objectMapper; - final String jsonFileName = "pipelineConfig.json"; final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/"; + @Autowired + PipelineProcessor processor; - @Scheduled(fixedRate = 25000) - public void scanFolders() { - logger.info("Scanning folders..."); - Path watchedFolderPath = Paths.get(watchedFoldersDir); - if (!Files.exists(watchedFolderPath)) { - try { - Files.createDirectories(watchedFolderPath); - logger.info("Created directory: {}", watchedFolderPath); - } catch (IOException e) { - logger.error("Error creating directory: {}", watchedFolderPath, e); - return; - } - } - try (Stream paths = Files.walk(watchedFolderPath)) { - paths.filter(Files::isDirectory).forEach(t -> { - try { - if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { - handleDirectory(t); - } - } catch (Exception e) { - logger.error("Error handling directory: {}", t, e); - } - }); - } catch (Exception e) { - logger.error("Error walking through directory: {}", watchedFolderPath, e); - } - } @Autowired ApplicationProperties applicationProperties; - - private void handleDirectory(Path dir) throws Exception { - logger.info("Handling directory: {}", dir); - Path jsonFile = dir.resolve(jsonFileName); - Path processingDir = dir.resolve("processing"); // Directory to move files during processing - if (!Files.exists(processingDir)) { - Files.createDirectory(processingDir); - logger.info("Created processing directory: {}", processingDir); - } - - if (Files.exists(jsonFile)) { - // Read JSON file - String jsonString; - try { - jsonString = new String(Files.readAllBytes(jsonFile)); - logger.info("Read JSON file: {}", jsonFile); - } catch (IOException e) { - logger.error("Error reading JSON file: {}", jsonFile, e); - return; - } - - // Decode JSON to PipelineConfig - PipelineConfig config; - try { - config = objectMapper.readValue(jsonString, PipelineConfig.class); - // Assuming your PipelineConfig class has getters for all necessary fields, you - // can perform checks here - if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) { - throw new IOException("Invalid JSON format"); - } - } catch (IOException e) { - logger.error("Error parsing PipelineConfig: {}", jsonString, e); - return; - } - - // For each operation in the pipeline - for (PipelineOperation operation : config.getOperations()) { - // Collect all files based on fileInput - File[] files; - String fileInput = (String) operation.getParameters().get("fileInput"); - if ("automated".equals(fileInput)) { - // If fileInput is "automated", process all files in the directory - try (Stream paths = Files.list(dir)) { - files = paths - .filter(path -> !Files.isDirectory(path)) // exclude directories - .filter(path -> !path.equals(jsonFile)) // exclude jsonFile - .map(Path::toFile) - .toArray(File[]::new); - - } catch (IOException e) { - e.printStackTrace(); - return; - } - } else { - // If fileInput contains a path, process only this file - files = new File[] { new File(fileInput) }; - } - - // Prepare the files for processing - List filesToProcess = new ArrayList<>(); - for (File file : files) { - logger.info(file.getName()); - logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName())); - Files.move(file.toPath(), processingDir.resolve(file.getName())); - filesToProcess.add(processingDir.resolve(file.getName()).toFile()); - } - - // Process the files - try { - List resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString); - - if(resources == null) { - return; - } - // Move resultant files and rename them as per config in JSON file - for (Resource resource : resources) { - String resourceName = resource.getFilename(); - String baseName = resourceName.substring(0, resourceName.lastIndexOf(".")); - String extension = resourceName.substring(resourceName.lastIndexOf(".")+1); - - String outputFileName = config.getOutputPattern().replace("{filename}", baseName); - - outputFileName = outputFileName.replace("{pipelineName}", config.getName()); - DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd"); - outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter)); - DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss"); - outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter)); - - outputFileName += "." + extension; - // {filename} {folder} {date} {tmime} {pipeline} - String outputDir = config.getOutputDir(); - - String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder(); - - if (outputFolder == null || outputFolder.isEmpty()) { - // If the environment variable is not set, use the default value - outputFolder = finishedFoldersDir; - } - logger.info("outputDir 0={}", outputDir); - // Replace the placeholders in the outputDir string - outputDir = outputDir.replace("{outputFolder}", outputFolder); - outputDir = outputDir.replace("{folderName}", dir.toString()); - logger.info("outputDir 1={}", outputDir); - outputDir = outputDir.replace("\\watchedFolders", ""); - outputDir = outputDir.replace("//watchedFolders", ""); - outputDir = outputDir.replace("\\\\watchedFolders", ""); - outputDir = outputDir.replace("/watchedFolders", ""); - - Path outputPath; - logger.info("outputDir 2={}", outputDir); - if (Paths.get(outputDir).isAbsolute()) { - // If it's an absolute path, use it directly - outputPath = Paths.get(outputDir); - } else { - // If it's a relative path, make it relative to the current working directory - outputPath = Paths.get(".", outputDir); - } - - logger.info("outputPath={}", outputPath); - - if (!Files.exists(outputPath)) { - try { - Files.createDirectories(outputPath); - logger.info("Created directory: {}", outputPath); - } catch (IOException e) { - logger.error("Error creating directory: {}", outputPath, e); - return; - } - } - logger.info("outputPath {}", outputPath); - logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString()); - File newFile = new File(outputPath.resolve(outputFileName).toString()); - OutputStream os = new FileOutputStream(newFile); - os.write(((ByteArrayResource)resource).getByteArray()); - os.close(); - logger.info("made {}", outputPath.resolve(outputFileName)); - } - - // If successful, delete the original files - for (File file : filesToProcess) { - Files.deleteIfExists(processingDir.resolve(file.getName())); - } - } catch (Exception e) { - // If an error occurs, move the original files back - for (File file : filesToProcess) { - Files.move(processingDir.resolve(file.getName()), file.toPath()); - } - throw e; - } - } - } - } - - List processFiles(List outputFiles, String jsonString) throws Exception { - - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonString); - - JsonNode pipelineNode = jsonNode.get("pipeline"); - logger.info("Running pipelineNode: {}", pipelineNode); - ByteArrayOutputStream logStream = new ByteArrayOutputStream(); - PrintStream logPrintStream = new PrintStream(logStream); - - boolean hasErrors = false; - - for (JsonNode operationNode : pipelineNode) { - String operation = operationNode.get("operation").asText(); - logger.info("Running operation: {}", operation); - JsonNode parametersNode = operationNode.get("parameters"); - String inputFileExtension = ""; - if (operationNode.has("inputFileType")) { - inputFileExtension = operationNode.get("inputFileType").asText(); - } else { - inputFileExtension = ".pdf"; - } - - List newOutputFiles = new ArrayList<>(); - boolean hasInputFileType = false; - - for (Resource file : outputFiles) { - if (file.getFilename().endsWith(inputFileExtension)) { - hasInputFileType = true; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("fileInput", file); - - Iterator> parameters = parametersNode.fields(); - while (parameters.hasNext()) { - Map.Entry parameter = parameters.next(); - body.add(parameter.getKey(), parameter.getValue().asText()); - } - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - HttpEntity> entity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - String url = "http://localhost:8080/" + operation; - - ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); - - // If the operation is filter and the response body is null or empty, skip this file - if (operation.startsWith("filter-") && (response.getBody() == null || response.getBody().length == 0)) { - logger.info("Skipping file due to failing {}", operation); - continue; - } - - if (!response.getStatusCode().equals(HttpStatus.OK)) { - logPrintStream.println("Error: " + response.getBody()); - hasErrors = true; - continue; - } - - - // Define filename - String filename; - if ("auto-rename".equals(operation)) { - // If the operation is "auto-rename", generate a new filename. - // This is a simple example of generating a filename using current timestamp. - // Modify as per your needs. - filename = "file_" + System.currentTimeMillis(); - } else { - // Otherwise, keep the original filename. - filename = file.getFilename(); - } - - // Check if the response body is a zip file - if (isZip(response.getBody())) { - // Unzip the file and add all the files to the new output files - newOutputFiles.addAll(unzip(response.getBody())); - } else { - Resource outputResource = new ByteArrayResource(response.getBody()) { - @Override - public String getFilename() { - return filename; - } - }; - newOutputFiles.add(outputResource); - } - } - - if (!hasInputFileType) { - logPrintStream.println( - "No files with extension " + inputFileExtension + " found for operation " + operation); - hasErrors = true; - } - - outputFiles = newOutputFiles; - } - logPrintStream.close(); - - } - if (hasErrors) { - logger.error("Errors occurred during processing. Log: {}", logStream.toString()); - } - return outputFiles; - } - - List handleFiles(File[] files, String jsonString) throws Exception { - if(files == null || files.length == 0) { - logger.info("No files"); - return null; - } - - logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); - - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonString); - - JsonNode pipelineNode = jsonNode.get("pipeline"); - - boolean hasErrors = false; - List outputFiles = new ArrayList<>(); - - for (File file : files) { - Path path = Paths.get(file.getAbsolutePath()); - System.out.println("Reading file: " + path); // debug statement - - if (Files.exists(path)) { - Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { - @Override - public String getFilename() { - return file.getName(); - } - }; - outputFiles.add(fileResource); - } else { - System.out.println("File not found: " + path); // debug statement - } - } - logger.info("Files successfully loaded. Starting processing..."); - return processFiles(outputFiles, jsonString); - } - - List handleFiles(MultipartFile[] files, String jsonString) throws Exception { - if(files == null || files.length == 0) { - logger.info("No files"); - return null; - } - logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length()); - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonString); - - JsonNode pipelineNode = jsonNode.get("pipeline"); - - boolean hasErrors = false; - List outputFiles = new ArrayList<>(); - - for (MultipartFile file : files) { - Resource fileResource = new ByteArrayResource(file.getBytes()) { - @Override - public String getFilename() { - return file.getOriginalFilename(); - } - }; - outputFiles.add(fileResource); - } - logger.info("Files successfully loaded. Starting processing..."); - return processFiles(outputFiles, jsonString); - } + @Autowired + private ObjectMapper objectMapper; + @PostMapping("/handleData") - public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) { - MultipartFile[] files = request.getFileInputs(); - String jsonString = request.getJsonString(); + 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 outputFiles = handleFiles(files, jsonString); - + 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); @@ -473,52 +114,4 @@ public class PipelineController { } } - private boolean isZip(byte[] data) { - if (data == null || data.length < 4) { - return false; - } - - // Check the first four bytes of the data against the standard zip magic number - return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; - } - - private List unzip(byte[] data) throws IOException { - logger.info("Unzipping data of length: {}", data.length); - List unzippedFiles = new ArrayList<>(); - - try (ByteArrayInputStream bais = new ByteArrayInputStream(data); - ZipInputStream zis = new ZipInputStream(bais)) { - - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int count; - - while ((count = zis.read(buffer)) != -1) { - baos.write(buffer, 0, count); - } - - final String filename = entry.getName(); - Resource fileResource = new ByteArrayResource(baos.toByteArray()) { - @Override - public String getFilename() { - return filename; - } - }; - - // If the unzipped file is a zip file, unzip it - if (isZip(baos.toByteArray())) { - logger.info("File {} is a zip file. Unzipping...", filename); - unzippedFiles.addAll(unzip(baos.toByteArray())); - } else { - unzippedFiles.add(fileResource); - } - } - } - - logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); - return unzippedFiles; - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java new file mode 100644 index 00000000..dc45d4cb --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -0,0 +1,258 @@ +package stirling.software.SPDF.controller.api.pipeline; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import stirling.software.SPDF.model.ApplicationProperties; +import stirling.software.SPDF.model.PipelineConfig; +import stirling.software.SPDF.model.PipelineOperation; + +@Service +public class PipelineDirectoryProcessor { + + private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class); + @Autowired + private ObjectMapper objectMapper; + @Autowired + private ApiDocService apiDocService; + @Autowired + private ApplicationProperties applicationProperties; + + final String watchedFoldersDir = "./pipeline/watchedFolders/"; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; + + @Autowired + PipelineProcessor processor; + + @Scheduled(fixedRate = 60000) + public void scanFolders() { + if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { + return; + } + Path watchedFolderPath = Paths.get(watchedFoldersDir); + if (!Files.exists(watchedFolderPath)) { + try { + Files.createDirectories(watchedFolderPath); + logger.info("Created directory: {}", watchedFolderPath); + } catch (IOException e) { + logger.error("Error creating directory: {}", watchedFolderPath, e); + return; + } + } + try (Stream paths = Files.walk(watchedFolderPath)) { + paths.filter(Files::isDirectory).forEach(t -> { + try { + if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) { + handleDirectory(t); + } + } catch (Exception e) { + logger.error("Error handling directory: {}", t, e); + } + }); + } catch (Exception e) { + logger.error("Error walking through directory: {}", watchedFolderPath, e); + } + } + + public void handleDirectory(Path dir) throws IOException { + logger.info("Handling directory: {}", dir); + Path processingDir = createProcessingDirectory(dir); + + Optional jsonFileOptional = findJsonFile(dir); + if (!jsonFileOptional.isPresent()) { + logger.warn("No .JSON settings file found. No processing will happen for dir {}.", dir); + return; + } + + Path jsonFile = jsonFileOptional.get(); + PipelineConfig config = readAndParseJson(jsonFile); + processPipelineOperations(dir, processingDir, jsonFile, config); + } + + private Path createProcessingDirectory(Path dir) throws IOException { + Path processingDir = dir.resolve("processing"); + if (!Files.exists(processingDir)) { + Files.createDirectory(processingDir); + logger.info("Created processing directory: {}", processingDir); + } + return processingDir; + } + + private Optional findJsonFile(Path dir) throws IOException { + try (Stream paths = Files.list(dir)) { + return paths.filter(file -> file.toString().endsWith(".json")).findFirst(); + } + } + + private PipelineConfig readAndParseJson(Path jsonFile) throws IOException { + String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8); + logger.debug("Reading JSON file: {}", jsonFile); + return objectMapper.readValue(jsonString, PipelineConfig.class); + } + + private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException { + for (PipelineOperation operation : config.getOperations()) { + validateOperation(operation); + File[] files = collectFilesForProcessing(dir, jsonFile, operation); + if(files == null || files.length == 0) { + logger.debug("No files detected for {} ", dir); + return; + } + List filesToProcess = prepareFilesForProcessing(files, processingDir); + runPipelineAgainstFiles(filesToProcess, config, dir, processingDir); + } + } + + private void validateOperation(PipelineOperation operation) throws IOException { + if (!apiDocService.isValidOperation(operation.getOperation(), operation.getParameters())) { + throw new IOException("Invalid operation: " + operation.getOperation()); + } + } + + private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException { + try (Stream paths = Files.list(dir)) { + if ("automated".equals(operation.getParameters().get("fileInput"))) { + return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile)) + .map(Path::toFile) + .toArray(File[]::new); + } else { + String fileInput = (String) operation.getParameters().get("fileInput"); + return new File[]{new File(fileInput)}; + } + } + } + + private List prepareFilesForProcessing(File[] files, Path processingDir) throws IOException { + List filesToProcess = new ArrayList<>(); + for (File file : files) { + Path targetPath = resolveUniqueFilePath(processingDir, file.getName()); + Files.move(file.toPath(), targetPath); + filesToProcess.add(targetPath.toFile()); + } + return filesToProcess; + } + + private Path resolveUniqueFilePath(Path directory, String originalFileName) { + Path filePath = directory.resolve(originalFileName); + int counter = 1; + + while (Files.exists(filePath)) { + String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")"); + filePath = directory.resolve(newName); + counter++; + } + + return filePath; + } + + private String appendSuffixToFileName(String originalFileName, String suffix) { + int dotIndex = originalFileName.lastIndexOf('.'); + if (dotIndex == -1) { + return originalFileName + suffix; + } else { + return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex); + } + } + + private void runPipelineAgainstFiles(List filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException { + try { + List inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0])); + if(inputFiles == null || inputFiles.size() == 0) { + return; + } + List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); + if (outputFiles == null) return; + moveAndRenameFiles(outputFiles, config, dir); + deleteOriginalFiles(filesToProcess, processingDir); + } catch (Exception e) { + logger.error("error during processing", e); + moveFilesBack(filesToProcess, processingDir); + } + } + + private void moveAndRenameFiles(List resources, PipelineConfig config, Path dir) throws IOException { + for (Resource resource : resources) { + String outputFileName = createOutputFileName(resource, config); + Path outputPath = determineOutputPath(config, dir); + + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + logger.info("Created directory: {}", outputPath); + } + + Path outputFile = outputPath.resolve(outputFileName); + try (OutputStream os = new FileOutputStream(outputFile.toFile())) { + os.write(((ByteArrayResource) resource).getByteArray()); + } + + logger.info("File moved and renamed to {}", outputFile); + } + } + + private String createOutputFileName(Resource resource, PipelineConfig config) { + String resourceName = resource.getFilename(); + String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); + String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); + + String outputFileName = config.getOutputPattern() + .replace("{filename}", baseName) + .replace("{pipelineName}", config.getName()) + .replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))) + .replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss"))) + + "." + extension; + + return outputFileName; + } + + private Path determineOutputPath(PipelineConfig config, Path dir) { + String outputDir = config.getOutputDir() + .replace("{outputFolder}", finishedFoldersDir) + .replace("{folderName}", dir.toString()) + .replaceAll("\\\\?watchedFolders", ""); + + return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); + } + + private void deleteOriginalFiles(List filesToProcess, Path processingDir) throws IOException { + for (File file : filesToProcess) { + Files.deleteIfExists(processingDir.resolve(file.getName())); + logger.info("Deleted original file: {}", file.getName()); + } + } + + private void moveFilesBack(List filesToProcess, Path processingDir) { + for (File file : filesToProcess) { + try { + Files.move(processingDir.resolve(file.getName()), file.toPath()); + logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName()); + } catch (IOException e) { + logger.error("Error moving file back to original location: {}", file.getName(), e); + } + } + } + + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java new file mode 100644 index 00000000..8b4b2ef4 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -0,0 +1,332 @@ +package stirling.software.SPDF.controller.api.pipeline; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import jakarta.servlet.ServletContext; + +import stirling.software.SPDF.SPdfApplication; +import stirling.software.SPDF.model.PipelineConfig; +import stirling.software.SPDF.model.PipelineOperation; +import stirling.software.SPDF.model.Role; + +@Service +public class PipelineProcessor { + + private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class); + + + @Autowired + private ApiDocService apiDocService; + + @Autowired(required=false) + private UserServiceInterface userService; + + @Autowired + private ServletContext servletContext; + + + + + private String getApiKeyForUser() { + if (userService == null) + return ""; + return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); + } + + + private String getBaseUrl() { + String contextPath = servletContext.getContextPath(); + String port = SPdfApplication.getPort(); + + return "http://localhost:" + port + contextPath + "/"; + } + + + + List runPipelineAgainstFiles(List outputFiles, PipelineConfig config) throws Exception { + + ByteArrayOutputStream logStream = new ByteArrayOutputStream(); + PrintStream logPrintStream = new PrintStream(logStream); + + boolean hasErrors = false; + + for (PipelineOperation pipelineOperation : config.getOperations()) { + String operation = pipelineOperation.getOperation(); + boolean isMultiInputOperation = apiDocService.isMultiInput(operation); + + logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation); + Map parameters = pipelineOperation.getParameters(); + String inputFileExtension = ""; + + //TODO + //if (operationNode.has("inputFileType")) { + // inputFileExtension = operationNode.get("inputFileType").asText(); + //} else { + inputFileExtension = ".pdf"; + //} + final String finalInputFileExtension = inputFileExtension; + + String url = getBaseUrl() + operation; + + List newOutputFiles = new ArrayList<>(); + if (!isMultiInputOperation) { + for (Resource file : outputFiles) { + boolean hasInputFileType = false; + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); + + + for(Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } + + ResponseEntity response = sendWebRequest(url, body); + + // If the operation is filter and the response body is null or empty, skip this + // file + if (operation.startsWith("filter-") + && (response.getBody() == null || response.getBody().length == 0)) { + logger.info("Skipping file due to failing {}", operation); + continue; + } + + if (!response.getStatusCode().equals(HttpStatus.OK)) { + logPrintStream.println("Error: " + response.getBody()); + hasErrors = true; + continue; + } + processOutputFiles(operation, file.getFilename(), response, newOutputFiles); + + } + + if (!hasInputFileType) { + logPrintStream.println( + "No files with extension " + inputFileExtension + " found for operation " + operation); + hasErrors = true; + } + + outputFiles = newOutputFiles; + } + + } else { + // Filter and collect all files that match the inputFileExtension + List matchingFiles = outputFiles.stream() + .filter(file -> file.getFilename().endsWith(finalInputFileExtension)) + .collect(Collectors.toList()); + + // Check if there are matching files + if (!matchingFiles.isEmpty()) { + // Create a new MultiValueMap for the request body + MultiValueMap body = new LinkedMultiValueMap<>(); + + // Add all matching files to the body + for (Resource file : matchingFiles) { + body.add("fileInput", file); + } + + for(Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } + + ResponseEntity response = sendWebRequest(url, body); + + // Handle the response + if (response.getStatusCode().equals(HttpStatus.OK)) { + processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles); + } else { + // Log error if the response status is not OK + logPrintStream.println("Error in multi-input operation: " + response.getBody()); + hasErrors = true; + } + } else { + logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation); + hasErrors = true; + } + } + logPrintStream.close(); + + } + if (hasErrors) { + logger.error("Errors occurred during processing. Log: {}", logStream.toString()); + } + return outputFiles; + } + + private ResponseEntity sendWebRequest(String url, MultiValueMap body ){ + RestTemplate restTemplate = new RestTemplate(); + + // Set up headers, including API key + HttpHeaders headers = new HttpHeaders(); + String apiKey = getApiKeyForUser(); + headers.add("X-API-Key", apiKey); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + + // Create HttpEntity with the body and headers + HttpEntity> entity = new HttpEntity<>(body, headers); + + // Make the request to the REST endpoint + return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); + } + + private List processOutputFiles(String operation, String fileName, ResponseEntity response, List newOutputFiles) throws IOException{ + // Define filename + String newFilename; + if ("auto-rename".equals(operation)) { + // If the operation is "auto-rename", generate a new filename. + // This is a simple example of generating a filename using current timestamp. + // Modify as per your needs. + newFilename = "file_" + System.currentTimeMillis(); + } else { + // Otherwise, keep the original filename. + newFilename = fileName; + } + + // Check if the response body is a zip file + if (isZip(response.getBody())) { + // Unzip the file and add all the files to the new output files + newOutputFiles.addAll(unzip(response.getBody())); + } else { + Resource outputResource = new ByteArrayResource(response.getBody()) { + @Override + public String getFilename() { + return newFilename; + } + }; + newOutputFiles.add(outputResource); + } + + return newOutputFiles; + + } + List generateInputFiles(File[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } + + + List outputFiles = new ArrayList<>(); + + for (File file : files) { + Path path = Paths.get(file.getAbsolutePath()); + logger.info("Reading file: " + path); // debug statement + + if (Files.exists(path)) { + Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) { + @Override + public String getFilename() { + return file.getName(); + } + }; + outputFiles.add(fileResource); + } else { + logger.info("File not found: " + path); + } + } + logger.info("Files successfully loaded. Starting processing..."); + return outputFiles; + } + + List generateInputFiles(MultipartFile[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } + + List outputFiles = new ArrayList<>(); + + for (MultipartFile file : files) { + Resource fileResource = new ByteArrayResource(file.getBytes()) { + @Override + public String getFilename() { + return file.getOriginalFilename(); + } + }; + outputFiles.add(fileResource); + } + logger.info("Files successfully loaded. Starting processing..."); + return outputFiles; + } + + private boolean isZip(byte[] data) { + if (data == null || data.length < 4) { + return false; + } + + // Check the first four bytes of the data against the standard zip magic number + return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; + } + + private List unzip(byte[] data) throws IOException { + logger.info("Unzipping data of length: {}", data.length); + List unzippedFiles = new ArrayList<>(); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(data); + ZipInputStream zis = new ZipInputStream(bais)) { + + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + + while ((count = zis.read(buffer)) != -1) { + baos.write(buffer, 0, count); + } + + final String filename = entry.getName(); + Resource fileResource = new ByteArrayResource(baos.toByteArray()) { + @Override + public String getFilename() { + return filename; + } + }; + + // If the unzipped file is a zip file, unzip it + if (isZip(baos.toByteArray())) { + logger.info("File {} is a zip file. Unzipping...", filename); + unzippedFiles.addAll(unzip(baos.toByteArray())); + } else { + unzippedFiles.add(fileResource); + } + } + } + + logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); + return unzippedFiles; + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java new file mode 100644 index 00000000..f1203be8 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java @@ -0,0 +1,4 @@ +package stirling.software.SPDF.controller.api.pipeline; +public interface UserServiceInterface { + String getApiKeyForUser(String username); +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index cba7ebb5..2c51e7d8 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.web; +import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -15,6 +16,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.UserRepository; @Controller @@ -46,14 +49,28 @@ public class AccountWebController { @PreAuthorize("hasRole('ROLE_ADMIN')") @GetMapping("/addUsers") public String showAddUserForm(Model model, Authentication authentication) { - List allUsers = userRepository.findAll(); + List allUsers = userRepository.findAll(); + Iterator iterator = allUsers.iterator(); + + while(iterator.hasNext()) { + User user = iterator.next(); + if(user != null) { + for (Authority authority : user.getAuthorities()) { + if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { + iterator.remove(); + break; // Break out of the inner loop once the user is removed + } + } + } + } + model.addAttribute("users", allUsers); model.addAttribute("currentUsername", authentication.getName()); return "addUsers"; } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @GetMapping("/account") public String account(HttpServletRequest request, Model model, Authentication authentication) { if (authentication == null || !authentication.isAuthenticated()) { @@ -100,7 +117,7 @@ public class AccountWebController { } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @GetMapping("/change-creds") public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { if (authentication == null || !authentication.isAuthenticated()) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 4b038e52..b670a129 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.web; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -41,31 +42,44 @@ public class GeneralWebController { model.addAttribute("currentPage", "pipeline"); List pipelineConfigs = new ArrayList<>(); - try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { - List jsonFiles = paths - .filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith(".json")) - .collect(Collectors.toList()); - - for (Path jsonFile : jsonFiles) { - String content = Files.readString(jsonFile, StandardCharsets.UTF_8); - pipelineConfigs.add(content); - } - List> pipelineConfigsWithNames = new ArrayList<>(); - for (String config : pipelineConfigs) { - Map jsonContent = new ObjectMapper().readValue(config, new TypeReference>(){}); - - String name = (String) jsonContent.get("name"); - Map configWithName = new HashMap<>(); - configWithName.put("json", config); - configWithName.put("name", name); + List> pipelineConfigsWithNames = new ArrayList<>(); + + if(new File("./pipeline/defaultWebUIConfigs/").exists()) { + try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { + List jsonFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".json")) + .collect(Collectors.toList()); + + for (Path jsonFile : jsonFiles) { + String content = Files.readString(jsonFile, StandardCharsets.UTF_8); + pipelineConfigs.add(content); + } + + for (String config : pipelineConfigs) { + Map jsonContent = new ObjectMapper().readValue(config, new TypeReference>(){}); + + String name = (String) jsonContent.get("name"); + Map configWithName = new HashMap<>(); + configWithName.put("json", config); + configWithName.put("name", name); + pipelineConfigsWithNames.add(configWithName); + } + + + + + } catch (IOException e) { + e.printStackTrace(); + } + } + if(pipelineConfigsWithNames.size() == 0) { + Map configWithName = new HashMap<>(); + configWithName.put("json", ""); + configWithName.put("name", "No preloaded configs found"); pipelineConfigsWithNames.add(configWithName); - } - model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); - - } catch (IOException e) { - e.printStackTrace(); } + model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); model.addAttribute("pipelineConfigs", pipelineConfigs); diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index 9dc4f2b7..dbb254a7 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -27,8 +27,8 @@ import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.model.ApplicationProperties; @RestController -@RequestMapping("/api/v1") -@Tag(name = "API", description = "Info APIs") +@RequestMapping("/api/v1/info") +@Tag(name = "Info", description = "Info APIs") public class MetricsController { diff --git a/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java new file mode 100644 index 00000000..1838b763 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java @@ -0,0 +1,42 @@ +package stirling.software.SPDF.model; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +public class ApiEndpoint { + private String name; + private Map parameters; + private String description; + + public ApiEndpoint(String name, JsonNode postNode) { + this.name = name; + this.parameters = new HashMap<>(); + postNode.path("parameters").forEach(paramNode -> { + String paramName = paramNode.path("name").asText(); + parameters.put(paramName, paramNode); + }); + this.description = postNode.path("description").asText(); + } + + public boolean areParametersValid(Map providedParams) { + for (String requiredParam : parameters.keySet()) { + if (!providedParams.containsKey(requiredParam)) { + return false; + } + } + return true; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]"; + } + + +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index dc068779..36073c88 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -106,6 +106,25 @@ public class ApplicationProperties { private Boolean enableLogin; private Boolean csrfDisabled; private InitialLogin initialLogin; + private int loginAttemptCount; + private long loginResetTimeMinutes; + + + public int getLoginAttemptCount() { + return loginAttemptCount; + } + + public void setLoginAttemptCount(int loginAttemptCount) { + this.loginAttemptCount = loginAttemptCount; + } + + public long getLoginResetTimeMinutes() { + return loginResetTimeMinutes; + } + + public void setLoginResetTimeMinutes(long loginResetTimeMinutes) { + this.loginResetTimeMinutes = loginResetTimeMinutes; + } public InitialLogin getInitialLogin() { return initialLogin != null ? initialLogin : new InitialLogin(); @@ -175,6 +194,19 @@ public class ApplicationProperties { private String rootURIPath; private String customStaticFilePath; private Integer maxFileSize; + + private Boolean enableAlphaFunctionality; + + + + + public Boolean getEnableAlphaFunctionality() { + return enableAlphaFunctionality; + } + + public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) { + this.enableAlphaFunctionality = enableAlphaFunctionality; + } public String getDefaultLocale() { return defaultLocale; @@ -218,12 +250,13 @@ public class ApplicationProperties { @Override public String toString() { - return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + ", rootURIPath=" - + rootURIPath + ", customStaticFilePath=" + customStaticFilePath + ", maxFileSize=" + maxFileSize - + "]"; + return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + + ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath + + ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]"; } + } public static class Ui { diff --git a/src/main/java/stirling/software/SPDF/model/AttemptCounter.java b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java new file mode 100644 index 00000000..fd668b05 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java @@ -0,0 +1,27 @@ +package stirling.software.SPDF.model; +public class AttemptCounter { + private int attemptCount; + private long lastAttemptTime; + + public AttemptCounter() { + this.attemptCount = 1; + this.lastAttemptTime = System.currentTimeMillis(); + } + + public void increment() { + this.attemptCount++; + this.lastAttemptTime = System.currentTimeMillis(); + } + + public int getAttemptCount() { + return attemptCount; + } + + public long getlastAttemptTime() { + return lastAttemptTime; + } + + public boolean shouldReset(long ATTEMPT_INCREMENT_TIME) { + return System.currentTimeMillis() - lastAttemptTime > ATTEMPT_INCREMENT_TIME; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java index 1b775de0..85315a16 100644 --- a/src/main/java/stirling/software/SPDF/model/Role.java +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -14,8 +14,13 @@ public enum Role { EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20), // 0 API calls per day and 20 web calls - WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20); + WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20), + + + INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE), + DEMO_USER("ROLE_DEMO_USER", 100, 100); + private final String roleId; private final int apiCallsPerDay; private final int webCallsPerDay; diff --git a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java index 1d7a8afe..d830ffeb 100644 --- a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java @@ -13,8 +13,8 @@ import lombok.NoArgsConstructor; public class HandleDataRequest { @Schema(description = "The input files") - private MultipartFile[] fileInputs; + private MultipartFile[] fileInput; @Schema(description = "JSON String") - private String jsonString; + private String json; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java index d53d8d12..34542da8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java @@ -5,6 +5,7 @@ import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; @@ -18,7 +19,7 @@ public class PDFWithPageNums extends PDFFile { @Schema(description = "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") private String pageNumbers; - + @Hidden public List getPageNumbersList(){ int pageCount = 0; try { @@ -30,6 +31,8 @@ public class PDFWithPageNums extends PDFFile { return GeneralUtils.parsePageString(pageNumbers, pageCount); } + + @Hidden public List getPageNumbersList(PDDocument doc){ int pageCount = 0; pageCount = doc.getNumberOfPages(); diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 2be68d08..038b8302 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -4,22 +4,17 @@ import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import javax.imageio.ImageIO; import javax.imageio.IIOImage; +import javax.imageio.ImageIO; import javax.imageio.ImageReader; -import javax.imageio.ImageWriter; import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import org.apache.pdfbox.pdmodel.PDDocument; diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java new file mode 100644 index 00000000..0046ee9f --- /dev/null +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -0,0 +1,16 @@ +package stirling.software.SPDF.utils; + +public class RequestUriUtils { + + public static boolean isStaticResource(String requestURI) { + + return requestURI.startsWith("/css/") + || requestURI.startsWith("/js/") + || requestURI.startsWith("/images/") + || requestURI.startsWith("/public/") + || requestURI.startsWith("/pdfjs/") + || requestURI.endsWith(".svg"); + + } + +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 00000000..e3188c2a --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + ____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____ +/ ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___| +\___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_ + ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| +|____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 90643fad..61b98ebd 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=إزالة الصفحات الفارغة home.removeBlanks.desc=يكتشف ويزيل الصفحات الفارغة من المستند removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=قارن home.compare.desc=يقارن ويظهر الاختلافات بين 2 من مستندات PDF compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=النسبة المئوية للصفحة التي removeBlanks.submit=إزالة الفراغات +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=يقارن compare.header=قارن ملفات PDF diff --git a/src/main/resources/messages_bg_BG.properties b/src/main/resources/messages_bg_BG.properties index f41dfe46..33052911 100644 --- a/src/main/resources/messages_bg_BG.properties +++ b/src/main/resources/messages_bg_BG.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Роля adminUserSettings.actions=Действия adminUserSettings.apiUser=Ограничен API потребител adminUserSettings.webOnlyUser=Само за уеб-потребител +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане adminUserSettings.submit=Съхранете потребителя @@ -255,6 +256,10 @@ home.removeBlanks.title=Премахване на празни страници home.removeBlanks.desc=Открива и премахва празни страници от документ removeBlanks.tags=почистване,рационализиране,без съдържание,организиране +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Сравнете home.compare.desc=Сравнява и показва разликите между 2 PDF документа compare.tags=разграничаване,контраст,промени,анализ @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Процент от страницата, коят removeBlanks.submit=Премахване на празни места +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Сравнявай compare.header=Сравнявай PDF-и diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index 02ec4c84..497a38ab 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Accions adminUserSettings.apiUser=Usuari amb API limitada adminUserSettings.webOnlyUser=Usuari només WEB +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Desar Usuari @@ -255,6 +256,10 @@ home.removeBlanks.title=Elimina les pàgines en blanc home.removeBlanks.desc=Detecta i elimina les pàgines en blanc d'un document removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Compara home.compare.desc=Compara i mostra les diferències entre 2 documents PDF compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentatge de pàgina que ha de ser blanca per el removeBlanks.submit=Elimina els espais en blanc +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Comparar compare.header=Compara PDF diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 30e8abf3..10e17038 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rolle adminUserSettings.actions=Aktion adminUserSettings.apiUser=Eingeschränkter API-Benutzer adminUserSettings.webOnlyUser=Nur Web-Benutzer +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern adminUserSettings.submit=Benutzer speichern @@ -255,6 +256,10 @@ home.removeBlanks.title=Leere Seiten entfernen home.removeBlanks.desc=Erkennt und entfernt leere Seiten aus einem Dokument removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Vergleichen home.compare.desc=Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Prozentsatz der Seite, die weiß sein muss, um ent removeBlanks.submit=Leere Seiten entfernen +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Vergleichen compare.header=PDFs vergleichen diff --git a/src/main/resources/messages_el_GR.properties b/src/main/resources/messages_el_GR.properties index a90e48f3..cb7fefa9 100644 --- a/src/main/resources/messages_el_GR.properties +++ b/src/main/resources/messages_el_GR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=\u03A1\u03CC\u03BB\u03BF\u03C2 adminUserSettings.actions=\u0395\u03BD\u03AD\u03C1\u03B3\u03B5\u03B9\u03B5\u03C2 adminUserSettings.apiUser=\u03A0\u03B5\u03C1\u03B9\u03BF\u03C1\u03B9\u03C3\u03BC\u03AD\u03BD\u03BF\u03C2 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03B3\u03B9\u03B1 \u03B4\u03B9\u03B5\u03C0\u03B1\u03C6\u03AE \u03C0\u03C1\u03BF\u03B3\u03C1\u03B1\u03BC\u03BC\u03B1\u03C4\u03B9\u03C3\u03BC\u03BF\u03CD \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03CE\u03BD (API User) adminUserSettings.webOnlyUser=\u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03BC\u03CC\u03BD\u03BF \u0399\u03C3\u03C4\u03BF\u03CD +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=\u0391\u03BD\u03B1\u03B3\u03BA\u03AC\u03C3\u03C4\u03B5 \u03C4\u03BF\u03BD \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7 \u03BD\u03B1 \u03B1\u03BB\u03BB\u03AC\u03BE\u03B5\u03B9 \u03C4\u03BF \u03CC\u03BD\u03BF\u03BC\u03B1 \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7/\u03BA\u03C9\u03B4\u03B9\u03BA\u03CC \u03C0\u03C1\u03CC\u03C3\u03B2\u03B1\u03C3\u03B7\u03C2 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7 \u03C3\u03CD\u03BD\u03B4\u03B5\u03C3\u03B7 adminUserSettings.submit=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7 @@ -255,6 +256,10 @@ home.removeBlanks.title=\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03BA\ home.removeBlanks.desc=\u0391\u03BD\u03AF\u03C7\u03B5\u03C5\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B1\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03BA\u03B5\u03BD\u03CE\u03BD \u03C3\u03B5\u03BB\u03AF\u03B4\u03C9\u03BD \u03B1\u03C0\u03CC \u03AD\u03BD\u03B1 \u03AD\u03B3\u03B3\u03C1\u03B1\u03C6\u03BF removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 home.compare.desc=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B5\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7 \u03C4\u03C9\u03BD \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03CE\u03BD \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD \u03B4\u03CD\u03BF PDF \u03B1\u03C1\u03C7\u03B5\u03AF\u03C9\u03BD compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=\u03A4\u03BF \u03C0\u03BF\u03C3\u03BF\u03C3\u03C4\ removeBlanks.submit=\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u039A\u03B5\u03BD\u03CE\u03BD +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 compare.header=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 PDFs diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 2148629e..3a21736d 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange = Force user to change username/password on login adminUserSettings.submit=Save User diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index 1a73c4d4..870b1ad7 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index fa476586..4a520a50 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Acciones adminUserSettings.apiUser=Usuario limitado de API adminUserSettings.webOnlyUser=Usuario solo web +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso adminUserSettings.submit=Guardar Usuario @@ -255,6 +256,10 @@ home.removeBlanks.title=Eliminar páginas en blanco home.removeBlanks.desc=Detectar y eliminar páginas en blanco de un documento removeBlanks.tags=limpieza,dinámica,sin contenido,organizar +home.removeAnnotations.title=Eliminar Anotaciones +home.removeAnnotations.desc=Eliminar todos los comentarios/anotaciones de un PDF +removeAnnotations.tags=comentarios,subrayar,notas,margen,eliminar + home.compare.title=Comparar home.compare.desc=Comparar y mostrar las diferencias entre 2 documentos PDF compare.tags=diferenciar,contrastar,cambios,análisis @@ -350,7 +355,7 @@ home.overlay-pdfs.title=Superponer PDFs home.overlay-pdfs.desc=Superponer PDFs encima de otro PDF overlay-pdfs.tags=Superponer -home.split-by-sections.title=Dividir PDF por Seccioned +home.split-by-sections.title=Dividir PDF por Secciones home.split-by-sections.desc=Dividir cada página de un PDF en secciones verticales y horizontales más pequeñas split-by-sections.tags=Dividir sección, Dividir, Personalizar @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Porcentaje de página que debe ser blanca para ser removeBlanks.submit=Eliminar espacios en blanco +#removeAnnotations +removeAnnotations.title=Eliminar anotaciones +removeAnnotations.header=Eliminar anotaciones +removeAnnotations.submit=Eliminar + + #compare compare.title=Comparar compare.header=Comparar archivos PDF @@ -861,7 +872,7 @@ split-by-size-or-count.submit=Enviar #overlay-pdfs overlay-pdfs.header=Superponer archivos PDF -overlay-pdfs.baseFile.label=Selleccione archivo PDF de base +overlay-pdfs.baseFile.label=Seleccione archivo PDF de base overlay-pdfs.overlayFiles.label=Seleccione archivos PDF a superponer overlay-pdfs.mode.label=Seleccione modo de superposición overlay-pdfs.mode.sequential=Superposición Sequencial diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index 69121553..642d7405 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Ekintzak adminUserSettings.apiUser=APIren erabiltzaile mugatua adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Gorde Erabiltzailea @@ -255,6 +256,10 @@ home.removeBlanks.title=Ezabatu orrialde zuriak home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Konparatu home.compare.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Zuria izan behar den orriaren ehunekoa ezabatua iz removeBlanks.submit=Ezabatu zuriuneak +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Konparatu compare.header=Konparatu PDF fitxategiak diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index b36ca85c..7969690c 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rôle adminUserSettings.actions=Actions adminUserSettings.apiUser=Utilisateur API limité adminUserSettings.webOnlyUser=Utilisateur Web uniquement +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Forcer l\u2019utilisateur à changer son nom d\u2019utilisateur/mot de passe lors de la connexion adminUserSettings.submit=Ajouter @@ -255,6 +256,10 @@ home.removeBlanks.title=Supprimer les pages vierges home.removeBlanks.desc=Détectez et supprimez les pages vierges d\u2019un PDF. removeBlanks.tags=pages vierges,supprimer,nettoyer,cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Comparer home.compare.desc=Comparez et visualisez les différences entre deux PDF. compare.tags=comparer,analyser,differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Pourcentage de la page qui doit contenir des pixel removeBlanks.submit=Supprimer les pages vierges +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Comparer compare.header=Comparer diff --git a/src/main/resources/messages_hi_IN.properties b/src/main/resources/messages_hi_IN.properties index 7ffcf677..57988512 100644 --- a/src/main/resources/messages_hi_IN.properties +++ b/src/main/resources/messages_hi_IN.properties @@ -1,7 +1,7 @@ ########### # Generic # ########### -# the direction that the language is written (ltr = left to right, rtl = right to left) +# the direction that the language is written (ltr=left to right, rtl = right to left) language.direction=ltr pdfPrompt=पीडीएफ़(फ़ाइलें) चुनें @@ -92,7 +92,7 @@ account.title=खाता सेटिंग्स account.accountSettings=खाता सेटिंग्स account.adminSettings=व्यवस्थापक सेटिंग्स - उपयोगकर्ताओं को देखें और जोड़ें account.userControlSettings=उपयोगकर्ता नियंत्रण सेटिंग्स -account.changeUsername=नया उपयोगकर्ता नाम +account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें account.password=पासवर्ड पुष्टि account.oldPassword=पुराना पासवर्ड @@ -119,6 +119,7 @@ adminUserSettings.role=रोल adminUserSettings.actions=क्रियाएँ adminUserSettings.apiUser=सीमित API उपयोगकर्ता adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=उपयोगकर्ता को लॉगिन पर उपयोगकर्ता नाम/पासवर्ड बदलने के लिए मजबूर करें adminUserSettings.submit=उपयोगकर्ता को सहेजें @@ -175,24 +176,24 @@ home.permissions.title=अनुमतियाँ बदलें home.permissions.desc=अपने पीडीएफ़ दस्तावेज़ की अनुमतियाँ बदलें permissions.tags=पढ़ें, लिखें, संपादित करें, प्रिंट + home.removePages.title=हटाएं home.removePages.desc=अपने पीडीएफ़ दस्तावेज़ से अनचाहे पृष्ठों को हटाएं। removePages.tags=पृष्ठ हटाएं, पृष्ठ मिटाएं + home.addPassword.title=पासवर्ड जोड़ें home.addPassword.desc=अपने पीडीएफ़ दस्तावेज़ को एक पासवर्ड से एन्क्रिप्ट करें। addPassword.tags=सुरक्षित, सुरक्षा + home.removePassword.title=पासवर्ड हटाएं home.removePassword.desc=अपने पीडीएफ़ दस्तावेज़ से पासवर्ड सुरक्षा को हटाएं। removePassword.tags=सुरक्षित, डिक्रिप्ट, सुरक्षा, पासवर्ड हटाएं, पासवर्ड मिटाएं -home.compressPdfs.title=Compress -home.compressPdfs.desc=Compress PDFs to reduce their file size. -compressPdfs.tags=squish,small,tiny - -home.compressPdfs.title=कम्प्रेस +home.compressPdfs.title=संकुचित करें (कम्प्रेस) home.compressPdfs.desc=फ़ाइल का आकार कम करने के लिए PDF को कम्प्रेस करें। compressPdfs.tags=स्क्विश, छोटा, छोटा + home.changeMetadata.title=मेटाडेटा बदलें home.changeMetadata.desc=PDF दस्तावेज़ से मेटाडेटा बदलें/हटाएं/जोड़ें। changeMetadata.tags=शीर्षक, लेखक, तारीख, निर्माण, समय, प्रकाशक, उत्पादक, आँकड़े @@ -205,6 +206,7 @@ home.ocr.title=OCR / स्कैन को साफ करें home.ocr.desc=स्कैन को साफ करता है और पीडीएफ़ में छवियों से पाठ को पहचानता है और टेक्स्ट के रूप में फिर से जोड़ता है। ocr.tags=पहचान, टेक्स्ट, छवि, स्कैन, पढ़ें, पहचान, पता लगाना, संपादनीय + home.extractImages.title=छवियां निकालें home.extractImages.desc=पीडीएफ़ से सभी छवियों को निकालता है और उन्हें ज़िप में सहेजता है extractImages.tags=चित्र, फोटो, सहेजें, संग्रह, ज़िप, कैप्चर, ग्रैब @@ -213,7 +215,6 @@ home.pdfToPDFA.title=PDF से PDF/A में home.pdfToPDFA.desc=लंबे समय के लिए स्टोरेज के लिए पीडीएफ़ को पीडीएफ़/ए में रूपांतरित करें pdfToPDFA.tags=संग्रह, लंबे समय के लिए, मानक, परिवर्तन, स्टोरेज, संरक्षण - home.PDFToWord.title=PDF से वर्ड में home.PDFToWord.desc=PDF को वर्ड प्रारूपों में रूपांतरित करें (DOC, DOCX और ODT) PDFToWord.tags=doc,docx,odt,word,परिवर्तन,प्रारूप,रूपांतरण,ऑफिस,माइक्रोसॉफ्ट,डॉक फ़ाइल @@ -230,6 +231,7 @@ home.PDFToHTML.title=PDF से HTML में home.PDFToHTML.desc=PDF को HTML प्रारूप में रूपांतरित करें PDFToHTML.tags=वेब सामग्री, ब्राउज़र अनुकूल + home.PDFToXML.title=PDF से XML में home.PDFToXML.desc=PDF को XML प्रारूप में रूपांतरित करें PDFToXML.tags=डेटा-निकालन, संरचित सामग्री, अंतरसंवाद, परिवर्तन, रूपांतरण @@ -238,7 +240,6 @@ home.ScannerImageSplit.title=स्कैन की गई फोटो का home.ScannerImageSplit.desc=एक फोटो/PDF के भीतर से कई फोटो को विभाजित करता है ScannerImageSplit.tags=अलग, ऑटो-डिटेक्ट, स्कैन, मल्टी-फोटो, संगठित - home.sign.title=हस्ताक्षर home.sign.desc=हस्ताक्षर को ड्राइंग, पाठ या छवि के रूप में पीडीएफ़ में जोड़ता है। sign.tags=अधिकृत करें, आदेश, ड्राइंग-हस्ताक्षर, पाठ-हस्ताक्षर, छवि-हस्ताक्षर @@ -331,10 +332,10 @@ home.PdfToSinglePage.title=पीडीएफ़ से एक बड़े प home.PdfToSinglePage.desc=सभी पीडीएफ़ पेजों को एक बड़े एकल पृष्ठ में मर्ज करता है PdfToSinglePage.tags=एकल पृष्ठ + home.showJS.title=जावास्क्रिप्ट दिखाएं home.showJS.desc=पीडीएफ़ में डाला गया कोई भी जावास्क्रिप्ट खोजता है और प्रदर्शित करता है -showJS.tags=जेएस - +showJS.tags=गोपनीयकरण, छिपाना, काला करना, काला, मार्कर, छिपा हुआ home.autoRedact.title=स्वतः गोपनीयकरण home.autoRedact.desc=प्रविष्ट पाठ के आधार पर पीडीएफ़ में पाठ को स्वतः गोपनीयकरित(काला करें) @@ -462,9 +463,9 @@ addPageNumbers.submit=पृष्ठ संख्या जोड़ें #auto-rename -auto-rename.title=Auto Rename -auto-rename.header=Auto Rename PDF -auto-rename.submit=Auto Rename +auto-rename.title=स्वतः नाम परिवर्तन (खुद ब खुद नाम बदलें) +auto-rename.header=स्वतः नाम परिवर्तन पीडीएफ़ +auto-rename.submit=स्वतः नाम परिवर्तन #adjustContrast @@ -508,6 +509,7 @@ pageLayout.pagesPerSheet=प्रति पृष्ठ पेज: pageLayout.addBorder=सीमा जोड़ें pageLayout.submit=प्रस्तुत क + #scalePages scalePages.title=पृष्ठ-स्केल समायोजित करें scalePages.header=पृष्ठ-स्केल समायोजित करें @@ -516,7 +518,6 @@ scalePages.scaleFactor=पृष्ठ का ज़ूम स्तर (क् scalePages.submit=प्रस्तुत करें - #certSign certSign.title=प्रमाणपत्र साइनिंग certSign.header=अपने प्रमाणपत्र के साथ एक पीडीएफ़ पर हस्ताक्षर करें (काम जारी है) @@ -591,7 +592,7 @@ ScannerImageSplit.selectText.8=फोटो के लिए न्यूनत ScannerImageSplit.selectText.9=बॉर्डर का आकार: ScannerImageSplit.selectText.10=निकालने और जोड़ने के लिए जोड़ा जाने वाला बॉर्डर का आकार सेट करता है ताकि आउटपुट में सफेद बॉर्डर न आए (डिफ़ॉल्ट: 1)। - + #OCR ocr.title=OCR / स्कैन सफाई ocr.header=स्कैन सफाई / OCR (ऑप्टिकल कैरेक्टर रिकग्निशन) @@ -635,7 +636,7 @@ compress.selectText.1=मैनुअल मोड - 1 से 4 तक compress.selectText.2=अनुकूलन स्तर: compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक) compress.selectText.4=स्वत: मोड - निर्धारित आकार पाने के लिए गुणवत्ता को स्वत: समायोजित करता है -compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB) +compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB) compress.submit=संकुचित करें @@ -710,8 +711,8 @@ imageToPDF.selectText.2=पीडीएफ को ऑटो रोटेट क imageToPDF.selectText.3=मल्टी फ़ाइल तर्क (केवल यदि कई छवियों के साथ काम किया जा रहा है) imageToPDF.selectText.4=एक ही पीडीएफ में मर्ज करें imageToPDF.selectText.5=अलग-अलग पीडीएफ में परिवर्तित करें - - + + #pdfToImage pdfToImage.title=पीडीएफ से छवि pdfToImage.header=पीडीएफ से छवि @@ -789,7 +790,7 @@ removePassword.submit=हटाएं #changeMetadata -changeMetadata.title=मेटाडेटा बदलें +changeMetadata.title=शीर्षक: changeMetadata.header=मेटाडेटा बदलें changeMetadata.selectText.1=कृपया उन चरों को संपादित करें जिन्हें आप बदलना चाहते हैं changeMetadata.selectText.2=सभी मेटाडेटा हटाएं diff --git a/src/main/resources/messages_hu_HU.properties b/src/main/resources/messages_hu_HU.properties index 0ae1324e..a4f47250 100644 --- a/src/main/resources/messages_hu_HU.properties +++ b/src/main/resources/messages_hu_HU.properties @@ -1,7 +1,7 @@ ########### # Generic # ########### -# the direction that the language is written (ltr = left to right, rtl = right to left) +# the direction that the language is written (ltr=left to right, rtl = right to left) language.direction=ltr pdfPrompt=Válasszon PDF-fájlokat @@ -19,7 +19,7 @@ save=Mentés close=Bezárás filesSelected=kiválasztott fájlok noFavourites=Nincs hozzáadva kedvenc -bored=Unatkozol? +bored=Unatkozol? alphabet=Ábécé downloadPdf=PDF letöltése text=Szöveg @@ -50,6 +50,7 @@ incorrectPasswordMessage=A jelenlegi jelszó helytelen. usernameExistsMessage=Az új felhasználónév már létezik. + ############# # NAVBAR # ############# @@ -60,7 +61,6 @@ navbar.darkmode=Sötét mód navbar.pageOps=Lap műveletek navbar.settings=Beállítások - ############# # SETTINGS # ############# @@ -76,6 +76,7 @@ settings.signOut=Kijelentkezés settings.accountSettings=Fiókbeállítások + changeCreds.title=Hitelesítés megváltoztatása changeCreds.header=Frissítse fiókadatait changeCreds.changeUserAndPassword=Alapértelmezett bejelentkezési adatokat használ. Adjon meg egy új jelszót (és felhasználónevet, ha szeretné) @@ -86,11 +87,13 @@ changeCreds.confirmNewPassword=Új jelszó megerősítése changeCreds.submit=Változtatások elküldése + account.title=Fiókbeállítások account.accountSettings=Fiókbeállítások account.adminSettings=Admin Beállítások - Felhasználók megtekintése és hozzáadása account.userControlSettings=Felhasználói vezérlési beállítások account.changeUsername=Új felhasználónév +account.changeUsername=Új felhasználónév account.password=Megerősítő jelszó account.oldPassword=Régi jelszó account.newPassword=Új jelszó @@ -116,16 +119,17 @@ adminUserSettings.role=Szerep adminUserSettings.actions=Műveletek adminUserSettings.apiUser=Korlátozott API-felhasználó adminUserSettings.webOnlyUser=Csak webes felhasználó +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Kényszerítse a felhasználót a felhasználónév/jelszó megváltoztatására bejelentkezéskor adminUserSettings.submit=Felhasználó mentése - ############# # HOME-PAGE # ############# home.desc=Lokálisan hostolt egyszerű megoldás minden PDF igényéhez. home.searchBar=Keresés funkciókra... + home.viewPdf.title=PDF Megtekintése home.viewPdf.desc=Megtekintés, annotálás, szöveg vagy képek hozzáadása viewPdf.tags=megtekintés,olvasás,annotálás,szöveg,kép @@ -140,12 +144,13 @@ merge.tags=egyesítés,Lapműveletek,Háttér,server oldal home.split.title=Osztás home.split.desc=PDF-ek felosztása több dokumentumra -split.tags=Lapműveletek,osztás,Több oldal,vágás,server oldal +split.tags=Lapműveletek,osztás,Több oldal,vágás,server oldal home.rotate.title=Forgatás home.rotate.desc=PDF-ek egyszerű forgatása. rotate.tags=server oldal + home.imageToPdf.title=Kép PDF-be home.imageToPdf.desc=Kép (PNG, JPEG, GIF) konvertálása PDF-fé. imageToPdf.tags=konverzió,img,jpg,kép,fotó @@ -158,6 +163,7 @@ home.pdfOrganiser.title=Szervezés home.pdfOrganiser.desc=Lapok eltávolítása/átszervezése bármilyen sorrendben pdfOrganiser.tags=duplex,páros,páratlan,rendezés,mozgatás + home.addImage.title=Kép hozzáadása home.addImage.desc=Kép hozzáadása a PDF megadott helyére addImage.tags=img,jpg,kép,fotó @@ -170,6 +176,7 @@ home.permissions.title=Engedélyek módosítása home.permissions.desc=Változtassa meg a PDF dokumentum engedélyeit permissions.tags=olvasás,írás,szerkesztés,nyomtatás + home.removePages.title=Eltávolítás home.removePages.desc=Szükségtelen lapok törlése a PDF dokumentumból. removePages.tags=Lapok eltávolítása,lapok törlése @@ -186,6 +193,7 @@ home.compressPdfs.title=Tömörítés home.compressPdfs.desc=PDF-ek tömörítése a fájlméret csökkentése érdekében. compressPdfs.tags=szorít,kicsi,miniatűr + home.changeMetadata.title=Metaadatok Módosítása home.changeMetadata.desc=Metaadatok Módosítása/Eltávolítása/Hozzáadása egy PDF dokumentumból changeMetadata.tags=Cím,szerző,dátum,alkotás,idő,közzétevő,gyártó,statisztika @@ -198,6 +206,7 @@ home.ocr.title=OCR / Tisztítás szkennelésekből home.ocr.desc=Tisztítás szkennelésekből és szöveg észlelése képeken belül egy PDF-ben, majd visszahozza szövegként. ocr.tags=felismerés,szöveg,kép,szken,gép,felismert,azonosítás,szerkeszthető + home.extractImages.title=Képek kinyerése home.extractImages.desc=Az összes kép kinyerése egy PDF-ből és mentése zip-be extractImages.tags=kép,fotó,mentés,archívum,zip,rögzítés,gyűjtés @@ -222,6 +231,7 @@ home.PDFToHTML.title=PDF >> HTML home.PDFToHTML.desc=PDF konvertálása HTML formátumra PDFToHTML.tags=web tartalom,böngészőbarát + home.PDFToXML.title=PDF >> XML home.PDFToXML.desc=PDF konvertálása XML formátumra PDFToXML.tags=adat-kinyerés,strukturált tartalom,interop,konverzió @@ -246,6 +256,10 @@ home.removeBlanks.title=Üres lapok eltávolítása home.removeBlanks.desc=Felismeri és eltávolítja az üres lapokat a dokumentumból removeBlanks.tags=takarítás,egyszerűsítés,nem-tartalom,szervez +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Összehasonlítás home.compare.desc=Összehasonlítja és megmutatja a különbségeket két PDF dokumentum között compare.tags=kiemel,ellentét,változások,elemzés @@ -271,7 +285,7 @@ home.add-page-numbers.desc=Lapszám hozzáadása a dokumentumhoz egy meghatároz add-page-numbers.tags=lapszámozás,címke,szervez,index home.auto-rename.title=Automatikus átnevezés PDF fájl -home.auto-rename.desc=Automatikusan átnevezi a PDF fájlt a felderített fejléc alapján +home.auto-rename.desc=Automatikusan átnevezi a PDF fájlt a felderített fejléc alapján auto-rename.tags=auto-felismerés,fejléc-alapú,szervezés,címkézés home.adjust-contrast.title=Színek/Kontraszt beállítása @@ -298,25 +312,30 @@ home.HTMLToPDF.title=HTML PDF-be home.HTMLToPDF.desc=Bármely HTML fájl vagy tömörített fájl átalakítása PDF-be HTMLToPDF.tags=markup,web-tartalom,transzformáció,konverzió + home.MarkdownToPDF.title=Markdown PDF-be home.MarkdownToPDF.desc=Bármely Markdown fájl átalakítása PDF-be MarkdownToPDF.tags=markup,web-tartalom,transzformáció,konverzió + home.getPdfInfo.title=Összes információ a PDF-ről home.getPdfInfo.desc=Az összes lehetséges információ beszerzése a PDF-ekről getPdfInfo.tags=információ,adat,statisztika,statisztika + home.extractPage.title=Lapok kinyerése home.extractPage.desc=Válassza ki a lapokat a PDF-ből extractPage.tags=kinyer + home.PdfToSinglePage.title=PDF egyetlen nagy lapba home.PdfToSinglePage.desc=Az összes PDF lap egyesítése egyetlen nagy lapba PdfToSinglePage.tags=egyetlen lap + home.showJS.title=JavaScript megjelenítése home.showJS.desc=Keres és megjelenít bármilyen JS-t, amit beinjektáltak a PDF-be -showJS.tags=JS +showJS.tags=Elrejt,Elrejtés,kitakarás,fekete,fekete,marker,elrejtett home.autoRedact.title=Automatikus Elrejtés home.autoRedact.desc=Automatikusan kitakar (elrejt) szöveget egy PDF-ben az input szöveg alapján @@ -326,10 +345,12 @@ home.tableExtraxt.title=PDF to CSV home.tableExtraxt.desc=Táblázatok kinyerése a PDF-ből CSV formátumra konvertálva tableExtraxt.tags=CSV,Táblázat kinyerése,kinyer,konvertál + home.autoSizeSplitPDF.title=Automatikus szétválasztás méret/számláló alapján home.autoSizeSplitPDF.desc=Egyetlen PDF szétválasztása több dokumentummá méret, oldalszám vagy dokumentum szám alapján autoSizeSplitPDF.tags=pdf,szétválasztás,dokumentum,szervezet + home.overlay-pdfs.title=PDF fájlok átlapolása home.overlay-pdfs.desc=PDF fájlok átlapolása egyik dokumentum a másik fölé helyezésével overlay-pdfs.tags=Átlapolás @@ -338,7 +359,6 @@ home.split-by-sections.title=PDF Szakaszokra osztása home.split-by-sections.desc=Minden oldal felosztása kisebb vízszintes és függőleges szakaszokra split-by-sections.tags=Szakasz elosztás, felosztás, testreszabás - ########################### # # # WEB PAGES # @@ -400,6 +420,7 @@ MarkdownToPDF.help=Az átalakítás folyamatban MarkdownToPDF.credit=WeasyPrint alkalmazása + #url-to-pdf URLToPDF.title=URL >> PDF URLToPDF.header=URL >> PDF @@ -523,6 +544,12 @@ removeBlanks.whitePercentDesc=Az oldalakon található 'fehér' pixelek százal removeBlanks.submit=Üres oldalak eltávolítása +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Összehasonlítás compare.header=PDF-ek összehasonlítása @@ -565,7 +592,7 @@ ScannerImageSplit.selectText.8=A fotók minimális kontúrterületének beállí ScannerImageSplit.selectText.9=Keret mérete: ScannerImageSplit.selectText.10=A hozzáadott és eltávolított keret méretének beállítása a fehér keretek elkerülése érdekében a kimeneten (alapértelmezett: 1). - + #OCR ocr.title=OCR / szkennelés tisztázása ocr.header=Szkennelés tisztázása / OCR (Optikai karakterfelismerés) @@ -639,12 +666,10 @@ pdfOrganiser.submit=Oldalak átrendezése multiTool.title=PDF többfunkciós eszköz multiTool.header=PDF többfunkciós eszköz - #view pdf viewPdf.title=PDF megtekintése viewPdf.header=PDF megtekintése - #pageRemover pageRemover.title=Oldaltörlő pageRemover.header=PDF oldaltörlő @@ -686,8 +711,8 @@ imageToPDF.selectText.2=Automatikus forgatás PDF imageToPDF.selectText.3=Több fájl logika (csak akkor engedélyezett, ha több képpel dolgozik) imageToPDF.selectText.4=Egyesítse egyetlen PDF-fé imageToPDF.selectText.5=Átalakítás különálló PDF-fé - - + + #pdfToImage pdfToImage.title=PDF képpé alakítása pdfToImage.header=PDF képpé alakítása @@ -765,7 +790,7 @@ removePassword.submit=Eltávolítás #changeMetadata -changeMetadata.title=Metaadatok módosítása +changeMetadata.title=Cím: changeMetadata.header=Metaadatok módosítása changeMetadata.selectText.1=Kérjük, szerkessze azokat a változókat, amelyeket módosítani szeretne changeMetadata.selectText.2=Minden metaadat törlése @@ -828,14 +853,12 @@ PDFToXML.header=PDF >> XML PDFToXML.credit=Ez a szolgáltatás a LibreOffice-t használja a fájlkonverzióhoz. PDFToXML.submit=Konvertálás - #PDFToCSV PDFToCSV.title=PDF >> CSV PDFToCSV.header=PDF >> CSV PDFToCSV.prompt=Válassza ki az oldalt a táblázat kinyeréséhez PDFToCSV.submit=Kinyerés - #split-by-size-or-count split-by-size-or-count.header=PDF felosztása méret vagy oldalszám alapján split-by-size-or-count.type.label=Válassza ki a felosztás típusát @@ -871,4 +894,3 @@ split-by-sections.vertical.label=Vízszintes szakaszok split-by-sections.horizontal.placeholder=Adja meg a vízszintes szakaszok számát split-by-sections.vertical.placeholder=Adja meg a függőleges szakaszok számát split-by-sections.submit=Felosztás - diff --git a/src/main/resources/messages_id_ID.properties b/src/main/resources/messages_id_ID.properties index 7c01ced1..8a00dd9f 100644 --- a/src/main/resources/messages_id_ID.properties +++ b/src/main/resources/messages_id_ID.properties @@ -25,7 +25,7 @@ downloadPdf=Unduh PDF text=Teks font=Jenis huruf selectFillter=-- Pilih -- -pageNum=Nomor Halaman +pageNum=Nomor Halaman sizes.small=Kecil sizes.medium=Sedang sizes.large=Besar @@ -92,7 +92,7 @@ account.title=Pengaturan Akun account.accountSettings=Pengaturan Akun account.adminSettings=Pengaturan Admin - Melihat dan Menambahkan Pengguna account.userControlSettings=Pengaturan Kontrol Pengguna -account.changeUsername=Nama Pengguna Baru +account.changeUsername=Ubah Nama Pengguna account.changeUsername=Ubah Nama Pengguna account.password=Konfirmasi Kata sandi account.oldPassword=Kata sandi lama @@ -119,6 +119,7 @@ adminUserSettings.role=Peran adminUserSettings.actions=Tindakan adminUserSettings.apiUser=Pengguna API Terbatas adminUserSettings.webOnlyUser=Pengguna Khusus Web +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Memaksa pengguna untuk mengubah nama pengguna/kata sandi saat masuk adminUserSettings.submit=Simpan Pengguna @@ -143,7 +144,7 @@ merge.tags=menggabungkan,Pengoperasian halaman,Back end,sisi server home.split.title=Membagi home.split.desc=Membagi PDF menjadi beberapa dokumen -split.tags=Pengoperasian halaman,membagi,Multi Halaman,memotong,sisi server +split.tags=Pengoperasian halaman,membagi,Multi Halaman,memotong,sisi server home.rotate.title=Putar home.rotate.desc=Memutar PDF Anda dengan mudah. @@ -331,9 +332,10 @@ home.PdfToSinglePage.title=PDF ke Satu Halaman Besar home.PdfToSinglePage.desc=Menggabungkan semua halaman PDF menjadi satu halaman besar PdfToSinglePage.tags=halaman tunggal + home.showJS.title=Tampilkan Javascript home.showJS.desc=Mencari dan menampilkan JS apa pun yang disuntikkan ke dalam PDF -showJS.tags=JS +showJS.tags=Hapus, Sembunyikan, padamkan, hitam, hitam, penanda, tersembunyi home.autoRedact.title=Redaksional Otomatis home.autoRedact.desc=Menyunting Otomatis (Menghitamkan) teks dalam PDF berdasarkan teks masukan @@ -409,6 +411,7 @@ getPdfInfo.header=Dapatkan Info tentang PDF getPdfInfo.submit=Dapatkan Info getPdfInfo.downloadJson=Unduh JSON + #markdown-to-pdf MarkdownToPDF.title=Markdown ke PDF MarkdownToPDF.header=Markdown Ke PDF @@ -417,6 +420,7 @@ MarkdownToPDF.help=Pekerjaan sedang berlangsung MarkdownToPDF.credit=Menggunakan WeasyPrint + #url-to-pdf URLToPDF.title=URL ke PDF URLToPDF.header=URL Ke PDF @@ -517,22 +521,22 @@ scalePages.submit=Kirim #certSign certSign.title=Penandatanganan Sertifikat certSign.header=Menandatangani PDF dengan sertifikat Anda (Sedang dalam proses) -certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan: -certSign.selectKey=Pilih Berkas Kunci Pribadi Anda (format PKCS # 8, bisa .pem atau .der): -certSign.selectCert=Pilih Berkas Sertifikat Anda (format X.509, bisa .pem atau .der): -certSign.selectP12=Pilih Berkas Keystore PKCS #12 Anda (.p12 atau .pfx) (Opsional, Jika disediakan, berkas tersebut harus berisi kunci pribadi dan sertifikat Anda): +certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan: +certSign.selectKey=Pilih Berkas Kunci Pribadi Anda (format PKCS # 8, bisa .pem atau .der): +certSign.selectCert=Pilih Berkas Sertifikat Anda (format X.509, bisa .pem atau .der): +certSign.selectP12=Pilih Berkas Keystore PKCS #12 Anda (.p12 atau .pfx) (Opsional, Jika disediakan, berkas tersebut harus berisi kunci pribadi dan sertifikat Anda): certSign.certType=Jenis Sertifikat -certSign.password=Masukkan Kata Sandi Kunci atau Kunci Pribadi Anda (Jika Ada): +certSign.password=Masukkan Kata Sandi Kunci atau Kunci Pribadi Anda (Jika Ada): certSign.showSig=Tampilkan Tanda Tangan certSign.reason=Alasan certSign.location=Lokasi -certSign.name=Nama +certSign.name=Nama certSign.submit=Tanda tangani PDF #removeBlanks removeBlanks.title=Hapus Halaman Kosong -hapusKosong.header=Hapus Halaman Kosong +removeBlanks.header=Remove Blank Pages removeBlanks.threshold=Ambang Batas Keputihan Piksel: removeBlanks.thresholdDesc=Ambang batas untuk menentukan seberapa putih piksel putih yang harus diklasifikasikan sebagai 'Putih'. 0=Hitam, 255 putih murni. removeBlanks.whitePercent=Persen Putih (%): @@ -553,6 +557,7 @@ compare.document.1=Dokumen 1 compare.document.2=Dokumen 2 compare.submit=Bandingkan + #sign sign.title=Tanda sign.header=Tandatangani PDF @@ -607,6 +612,7 @@ ocr.help=Silakan baca dokumentasi ini tentang cara menggunakan ini untuk bahasa ocr.credit=Layanan ini menggunakan OCRmyPDF dan Tesseract untuk OCR. ocr.submit=Memproses PDF dengan OCR + #extractImages extractImages.title=Ekstrak Gambar extractImages.header=Mengekstrak Gambar @@ -616,7 +622,7 @@ extractImages.submit=Ekstrak #File to PDF fileToPDF.title=Berkas ke PDF -fileToPDF.header=Mengonversi berkas apa pun ke PDF +fileToPDF.header=Mengonversi berkas apa pun ke PDF fileToPDF.credit=Layanan ini menggunakan LibreOffice dan Unoconv untuk konversi berkas. fileToPDF.supportedFileTypes=Jenis berkas yang didukung harus mencakup yang di bawah ini, namun untuk daftar lengkap format yang didukung, silakan lihat dokumentasi LibreOffice fileToPDF.submit=Konversi ke PDF @@ -630,7 +636,7 @@ compress.selectText.1=Mode Manual - Dari 1 hingga 4 compress.selectText.2=Tingkat Optimalisasi: compress.selectText.3=4 (Buruk untuk gambar teks) compress.selectText.4=Mode Otomatis - Menyesuaikan kualitas secara otomatis untuk mendapatkan PDF dengan ukuran yang tepat -compress.selectText.5=Ukuran PDF yang diharapkan (mis. 25MB, 10,8MB, 25KB) +compress.selectText.5=Ukuran PDF yang diharapkan (mis. 25MB, 10,8MB, 25KB) compress.submit=Kompres @@ -750,7 +756,7 @@ watermark.selectText.1=Pilih PDF untuk menambahkan watermark: watermark.selectText.2=Text Watermark: watermark.selectText.3=Ukuran Huruf: watermark.selectText.4=Rotasi (0-360): -watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal): +watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal): watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal): watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.8=Tipe Watermark: @@ -784,7 +790,7 @@ removePassword.submit=Hapus #changeMetadata -changeMetadata.title=Ganti Metadata +changeMetadata.title=Judul: changeMetadata.header=Ganti Metadata changeMetadata.selectText.1=Silakan edit variabel yang ingin Anda ubah changeMetadata.selectText.2=Hapus semua metadata @@ -855,7 +861,7 @@ PDFToCSV.submit=Ektraksi #split-by-size-or-count split-by-size-or-count.header=Pisahkan PDF berdasarkan ukuran atau jumlah -split-by-size-or-count.type.label= Pilih Tipe Split +split-by-size-or-count.type.label=Pilih Tipe Split split-by-size-or-count.type.size=Berdasarkan Ukuran split-by-size-or-count.type.pageCount=Berdasarkan Jumlah Halaman split-by-size-or-count.type.docCount=Berdasarkan Jumlah Dokumen diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 52f106a3..02cf6314 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Ruolo adminUserSettings.actions=Azioni adminUserSettings.apiUser=Utente API limitato adminUserSettings.webOnlyUser=Utente solo Web +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Forza l'utente a cambiare nome username/password all'accesso adminUserSettings.submit=Salva utente @@ -255,6 +256,10 @@ home.removeBlanks.title=Rimuovi pagine vuote home.removeBlanks.desc=Trova e rimuovi pagine vuote da un PDF. removeBlanks.tags=pulire,semplificare,non contenere contenuti,organizzare +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Compara home.compare.desc=Vedi e compara le differenze tra due PDF. compare.tags=differenziare,contrastare,cambiare,analisi @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentuale della pagina che deve essere bianca pe removeBlanks.submit=Rimuovi +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Compara compare.header=Compara PDF diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index 04ad2da4..e4cb1ff6 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -119,6 +119,7 @@ adminUserSettings.role=役割 adminUserSettings.actions=アクション adminUserSettings.apiUser=限定されたAPIユーザー adminUserSettings.webOnlyUser=ウェブ専用ユーザー +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する adminUserSettings.submit=ユーザーの保存 @@ -255,6 +256,10 @@ home.removeBlanks.title=空白ページの削除 home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。 removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=比較 home.compare.desc=2つのPDFを比較して表示します。 compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=削除するページの白の割合 removeBlanks.submit=空白ページの削除 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=比較 compare.header=PDFの比較 diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index 429575c4..01f15e66 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=역할 adminUserSettings.actions=동작 adminUserSettings.apiUser=제한된 API 사용 adminUserSettings.webOnlyUser=웹 사용만 허용 +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=다음 로그인 때 사용자명과 비밀번호를 변경하도록 강제 adminUserSettings.submit=사용자 저장 @@ -255,6 +256,10 @@ home.removeBlanks.title=빈 페이지 제거 home.removeBlanks.desc=PDF 문서에서 빈 페이지를 감지하고 제거합니다. removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=비교 home.compare.desc=2개의 PDF 문서를 비교하고 차이를 표시합니다. compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=제거될 페이지의 흰색 픽셀 비율 removeBlanks.submit=빈 페이지 제거 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=비교 compare.header=PDF 문서 비교 diff --git a/src/main/resources/messages_nl_NL.properties b/src/main/resources/messages_nl_NL.properties index 262286bd..17087c07 100644 --- a/src/main/resources/messages_nl_NL.properties +++ b/src/main/resources/messages_nl_NL.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Acties adminUserSettings.apiUser=Beperkte API gebruiker adminUserSettings.webOnlyUser=Alleen web gebruiker +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Sla gebruiker op @@ -255,6 +256,10 @@ home.removeBlanks.title=Verwijder lege pagina''s home.removeBlanks.desc=Detecteert en verwijdert lege pagina''s uit een document removeBlanks.tags=opruimen,stroomlijnen,geen-inhoud,organiseren +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Vergelijken home.compare.desc=Vergelijkt en toont de verschillen tussen 2 PDF-documenten compare.tags=onderscheiden,contrasteren,veranderingen,analyse @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentage van de pagina dat ''witte'' pixels moet removeBlanks.submit=Blanco''s verwijderen +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Vergelijken compare.header=PDF''s vergelijken diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 4ba1c66f..1a725f65 100644 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=Usuń puste strony home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Porównaj home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Procent strony, która musi być biała, aby zosta removeBlanks.submit=Usuń puste +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Porównaj compare.header=Porównaj PDF(y) diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index 17d16dcb..c29c8f24 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=Удалить пустые страницы home.removeBlanks.desc=Обнаруживает и удаляет пустые страницы из документа removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Сравнение home.compare.desc=Сравнивает и показывает различия между двумя PDF-документами compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Общий процент белого на стр removeBlanks.submit=Удалить Пустые +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Сравнение compare.header=Сравнение PDFы diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index 780a3fb1..d090ef8e 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=Ta bort tomma sidor home.removeBlanks.desc=Känner av och tar bort tomma sidor från ett dokument removeBlanks.tags=cleanup,streamline,non-content,organize +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Jämför home.compare.desc=Jämför och visar skillnaderna mellan 2 PDF-dokument compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Procentandel av sidan som måste vara vit för att removeBlanks.submit=Ta bort tomrum +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Jämför compare.header=Jämför PDF-filer diff --git a/src/main/resources/messages_tr_TR.properties b/src/main/resources/messages_tr_TR.properties index 098f491d..43eab312 100644 --- a/src/main/resources/messages_tr_TR.properties +++ b/src/main/resources/messages_tr_TR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Eylemler adminUserSettings.apiUser=Sınırlı API Kullanıcısı adminUserSettings.webOnlyUser=Sadece Web Kullanıcısı +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Kullanıcının girişte kullanıcı adı/şifre değiştirmesini zorla adminUserSettings.submit=Kullanıcıyı Kaydet @@ -255,6 +256,10 @@ home.removeBlanks.title=Boş Sayfaları Kaldır home.removeBlanks.desc=Bir belgeden boş sayfaları tespit eder ve kaldırır removeBlanks.tags=temizle,sadeleştir,içeriksiz,düzenle +home.removeAnnotations.title=Remove Annotations +home.removeAnnotations.desc=Removes all comments/annotations from a PDF +removeAnnotations.tags=comments,highlight,notes,markup,remove + home.compare.title=Karşılaştır home.compare.desc=2 PDF Belgesi arasındaki farkları karşılaştırır ve gösterir compare.tags=farklılaştır,karşılaştır,değişiklikler,analiz @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Bir sayfanın 'beyaz' pixel olması gereken yüzde removeBlanks.submit=Boşları Kaldır +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Karşılaştır compare.header=PDF'leri Karşılaştır diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index f5ec2339..808fac2c 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -119,6 +119,7 @@ adminUserSettings.role=角色 adminUserSettings.actions=操作 adminUserSettings.apiUser=有限 API 用户 adminUserSettings.webOnlyUser=仅限 Web 用户 +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=强制用户在登录时更改用户名/密码 adminUserSettings.submit=保存用户 @@ -126,12 +127,12 @@ adminUserSettings.submit=保存用户 # HOME-PAGE # ############# home.desc=CZL一站式服务,满足您的所有PDF需求。 -home.searchBar=Search for features... +home.searchBar=搜索您需要的功能... -home.viewPdf.title=View PDF -home.viewPdf.desc=View, annotate, add text or images -viewPdf.tags=view,read,annotate,text,image +home.viewPdf.title=浏览PDF +home.viewPdf.desc=浏览、注释、添加文本或图像 +viewPdf.tags=浏览、阅读、注释、文本、图像 home.multiTool.title=PDF多功能工具 home.multiTool.desc=合并、旋转、重新排列和删除PDF页面 @@ -255,6 +256,10 @@ home.removeBlanks.title=删除空白页 home.removeBlanks.desc=检测并删除文档中的空白页 removeBlanks.tags=清理、简化、非内容、整理 +home.removeAnnotations.title=删除标注 +home.removeAnnotations.desc=删除PDF中的所有标注/评论 +removeAnnotations.tags=评论、高亮、笔记、标注、删除 + home.compare.title=比较 home.compare.desc=比较并显示两个PDF文档之间的差异 compare.tags=区分、对比、更改、分析 @@ -337,22 +342,22 @@ home.autoRedact.desc=根据输入文本自动删除(覆盖)PDF中的文本 showJS.tags=JavaScript home.tableExtraxt.title=PDF to CSV -home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV -tableExtraxt.tags=CSV,Table Extraction,extract,convert +home.tableExtraxt.desc=从PDF中提取表格并将其转换为CSV +tableExtraxt.tags=CSV、表格提取、提取、转换 -home.autoSizeSplitPDF.title=Auto Split by Size/Count -home.autoSizeSplitPDF.desc=Split a single PDF into multiple documents based on size, page count, or document count -autoSizeSplitPDF.tags=pdf,split,document,organization +home.autoSizeSplitPDF.title=自动根据大小/数目拆分PDF +home.autoSizeSplitPDF.desc=将单个PDF拆分为多个文档,基于大小、页数或文档数 +autoSizeSplitPDF.tags=pdf、拆分、文件、组织 -home.overlay-pdfs.title=Overlay PDFs -home.overlay-pdfs.desc=Overlays PDFs on-top of another PDF -overlay-pdfs.tags=Overlay +home.overlay-pdfs.title=叠加PDF +home.overlay-pdfs.desc=将PDF叠加在另一个PDF上 +overlay-pdfs.tags=叠加 -home.split-by-sections.title=Split PDF by Sections -home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections -split-by-sections.tags=Section Split, Divide, Customize +home.split-by-sections.title=拆分PDF成小块 +home.split-by-sections.desc=将PDF的每一页分割成更小的水平和垂直的部分 +split-by-sections.tags=章节拆分、分割、自定义 ########################### # # @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=必须为白色才能删除的页面百分比 removeBlanks.submit=删除空白 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=比较 compare.header=比较 PDF @@ -843,43 +854,43 @@ PDFToXML.credit=此服务使用LibreOffice进行文件转换。 PDFToXML.submit=转换 #PDFToCSV -PDFToCSV.title=PDF ? CSV -PDFToCSV.header=PDF ? CSV -PDFToCSV.prompt=Choose page to extract table -PDFToCSV.submit=?? +PDFToCSV.title=PDF To CSV +PDFToCSV.header=将 PDF 转换为 CSV +PDFToCSV.prompt=选择需要提取表格的页面 +PDFToCSV.submit=提取 #split-by-size-or-count -split-by-size-or-count.header=Split PDF by Size or Count -split-by-size-or-count.type.label=Select Split Type -split-by-size-or-count.type.size=By Size -split-by-size-or-count.type.pageCount=By Page Count -split-by-size-or-count.type.docCount=By Document Count -split-by-size-or-count.value.label=Enter Value -split-by-size-or-count.value.placeholder=Enter size (e.g., 2MB or 3KB) or count (e.g., 5) -split-by-size-or-count.submit=Submit +split-by-size-or-count.header=按照大小或数目拆分PDF +split-by-size-or-count.type.label=选择拆分类型 +split-by-size-or-count.type.size=按照大小 +split-by-size-or-count.type.pageCount=按照页数 +split-by-size-or-count.type.docCount=按照文档数 +split-by-size-or-count.value.label=输入数值 +split-by-size-or-count.value.placeholder=输入大小(例如,2MB或3KB)或数目(例如,5) +split-by-size-or-count.submit=提交 #overlay-pdfs -overlay-pdfs.header=Overlay PDF Files -overlay-pdfs.baseFile.label=Select Base PDF File -overlay-pdfs.overlayFiles.label=Select Overlay PDF Files -overlay-pdfs.mode.label=Select Overlay Mode -overlay-pdfs.mode.sequential=Sequential Overlay -overlay-pdfs.mode.interleaved=Interleaved Overlay -overlay-pdfs.mode.fixedRepeat=Fixed Repeat Overlay -overlay-pdfs.counts.label=Overlay Counts (for Fixed Repeat Mode) -overlay-pdfs.counts.placeholder=Enter comma-separated counts (e.g., 2,3,1) -overlay-pdfs.position.label=Select Overlay Position -overlay-pdfs.position.foreground=Foreground -overlay-pdfs.position.background=Background -overlay-pdfs.submit=Submit +overlay-pdfs.header=叠加PDF文件 +overlay-pdfs.baseFile.label=选择基础PDF文件 +overlay-pdfs.overlayFiles.label=选择需要叠加在基础上的PDF文件 +overlay-pdfs.mode.label=选择叠加模式 +overlay-pdfs.mode.sequential=按顺序叠加 +overlay-pdfs.mode.interleaved=交错叠加 +overlay-pdfs.mode.fixedRepeat=固定重复叠加 +overlay-pdfs.counts.label=叠加次数(仅限固定重复叠加模式) +overlay-pdfs.counts.placeholder=输入用逗号分隔的次数(例如,2,3,1) +overlay-pdfs.position.label=选择叠加位置 +overlay-pdfs.position.foreground=前面(上面) +overlay-pdfs.position.background=后面(下面) +overlay-pdfs.submit=提交 #split-by-sections -split-by-sections.title=Split PDF by Sections -split-by-sections.header=Split PDF into Sections -split-by-sections.horizontal.label=Horizontal Divisions -split-by-sections.vertical.label=Vertical Divisions -split-by-sections.horizontal.placeholder=Enter number of horizontal divisions -split-by-sections.vertical.placeholder=Enter number of vertical divisions -split-by-sections.submit=Split PDF +split-by-sections.title=按照块(Section)拆分PDF +split-by-sections.header=将PDF拆分成块 +split-by-sections.horizontal.label=水平分割 +split-by-sections.vertical.label=垂直分割 +split-by-sections.horizontal.placeholder=输入水平分割数 +split-by-sections.vertical.placeholder=输入垂直分割数 +split-by-sections.submit=分割PDF diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 0f229ba4..52d5e4de 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -5,10 +5,13 @@ security: enableLogin: false # set to 'true' to enable login csrfDisabled: true - + loginAttemptCount: 5 # lock user account after 5 tries + loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts + system: defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow + enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes) #ui: # appName: exampleAppName # Application's visible name diff --git a/src/main/resources/static/css/dark-mode.css b/src/main/resources/static/css/dark-mode.css index 82e01676..2f070c9c 100644 --- a/src/main/resources/static/css/dark-mode.css +++ b/src/main/resources/static/css/dark-mode.css @@ -75,6 +75,13 @@ table th, table td { border: none; color: #fff !important; } + +.btn-warning { + background-color: #ffc107 !important; + border: none; + color: #000 !important; +} + .btn-outline-secondary { color: #fff !important; border-color: #fff; @@ -92,6 +99,11 @@ hr { background-color: rgba(255, 255, 255, 0.6); /* for some browsers that might use background instead of border for
*/ } +.modal-content { + color: #fff !important; + border-color: #fff; +} + #global-buttons-container input { background-color: #323948; caret-color: #ffffff; diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css index ab3b3348..fe184637 100644 --- a/src/main/resources/static/css/home.css +++ b/src/main/resources/static/css/home.css @@ -1,5 +1,5 @@ #searchBar { - background-image: url('/images/search.svg'); + background-image: url('../images/search.svg'); background-position: 16px 16px; background-repeat: no-repeat; width: 100%; diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js index 06810743..4fcde3a0 100644 --- a/src/main/resources/static/js/pipeline.js +++ b/src/main/resources/static/js/pipeline.js @@ -12,20 +12,17 @@ function validatePipeline() { if (currentOperation === '/add-password') { containsAddPassword = true; } - console.log(currentOperation); - console.log(apiDocs[currentOperation]); + let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; - console.log("currentOperationDescription", currentOperationDescription); - console.log("nextOperationDescription", nextOperationDescription); - + // Strip off 'ZIP-' prefix + currentOperationDescription = currentOperationDescription.replace("ZIP-", ''); + nextOperationDescription = nextOperationDescription.replace("ZIP-", ''); + let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; - console.log("Operation " + currentOperation + " Output: " + currentOperationOutput); - console.log("Operation " + nextOperation + " Input: " + nextOperationInput); - // Splitting in case of multiple possible output/input let currentOperationOutputArr = currentOperationOutput.split('/'); let nextOperationInputArr = nextOperationInput.split('/'); @@ -35,6 +32,7 @@ function validatePipeline() { console.log(`Intersection: ${intersection}`); if (intersection.length === 0) { + updateValidateButton(false); isValid = false; console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); @@ -43,6 +41,7 @@ function validatePipeline() { } } if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { + updateValidateButton(false); alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); return false; } @@ -53,10 +52,20 @@ function validatePipeline() { console.error('Pipeline is not valid'); // Stop operation, maybe display an error to the user } - + updateValidateButton(isValid); return isValid; } +function updateValidateButton(isValid) { + var validateButton = document.getElementById('validateButton'); + if (isValid) { + validateButton.classList.remove('btn-danger'); + validateButton.classList.add('btn-success'); + } else { + validateButton.classList.remove('btn-success'); + validateButton.classList.add('btn-danger'); + } +} @@ -67,14 +76,14 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() return; } let selectedOperation = document.getElementById('operationsDropdown').value; - let parameters = operationSettings[selectedOperation] || {}; + + + var pipelineName = document.getElementById('pipelineName').value; + let pipelineList = document.getElementById('pipelineList').children; let pipelineConfig = { - "name": "uniquePipelineName", - "pipeline": [{ - "operation": selectedOperation, - "parameters": parameters - }], + "name": pipelineName, + "pipeline": [], "_examples": { "outputDir": "{outputFolder}/{folderName}", "outputFileName": "{filename}-{pipelineName}-{date}-{time}" @@ -83,6 +92,28 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() "outputFileName": "{filename}" }; + for (let i = 0; i < pipelineList.length; i++) { + let operationName = pipelineList[i].querySelector('.operationName').textContent; + let parameters = operationSettings[operationName] || {}; + + pipelineConfig.pipeline.push({ + "operation": operationName, + "parameters": parameters + }); + } + + + + + + + + + + + + + let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); let formData = new FormData(); @@ -99,37 +130,50 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() formData.append('json', pipelineConfigJson); console.log("formData", formData); - fetch('/handleData', { - method: 'POST', - body: formData + fetch('api/v1/pipeline/handleData', { + method: 'POST', + body: formData }) - .then(response => response.blob()) - .then(blob => { + .then(response => { + // Save the response to use it later + const responseToUseLater = response; + + return response.blob().then(blob => { + let url = window.URL.createObjectURL(blob); + let a = document.createElement('a'); + a.href = url; + + // Use responseToUseLater instead of response + const contentDisposition = responseToUseLater.headers.get('Content-Disposition'); + let filename = 'download'; + if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) { + filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim(); + } + a.download = filename; + + document.body.appendChild(a); + a.click(); + a.remove(); + }); + }) + .catch((error) => { + console.error('Error:', error); + }); - let url = window.URL.createObjectURL(blob); - let a = document.createElement('a'); - a.href = url; - a.download = 'outputfile'; - document.body.appendChild(a); - a.click(); - a.remove(); - }) - .catch((error) => { - console.error('Error:', error); - }); }); let apiDocs = {}; - +let apiSchemas = {}; let operationSettings = {}; -fetch('v3/api-docs') +fetch('v1/api-docs') .then(response => response.json()) .then(data => { apiDocs = data.paths; + apiSchemas = data.components.schemas; let operationsDropdown = document.getElementById('operationsDropdown'); - const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here + const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here operationsDropdown.innerHTML = ''; @@ -138,6 +182,9 @@ fetch('v3/api-docs') // Group operations by tags Object.keys(data.paths).forEach(operationPath => { let operation = data.paths[operationPath].post; + if(!operation || !operation.description) { + console.log(operationPath); + } if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) { let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag if (!operationsByTag[operationTag]) { @@ -146,9 +193,8 @@ fetch('v3/api-docs') operationsByTag[operationTag].push(operationPath); } }); - // Specify the order of tags - let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; + let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"]; // Create dropdown options tagOrder.forEach(tag => { @@ -158,8 +204,18 @@ fetch('v3/api-docs') operationsByTag[tag].forEach(operationPath => { let option = document.createElement('option'); - let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes - option.textContent = operationWithoutSlash; + + let operationPathDisplay = operationPath + operationPathDisplay = operationPath.replace(new RegExp("api/v1/" + tag.toLowerCase() + "/", 'i'), ""); + + + if(operationPath.includes("/convert")){ + operationPathDisplay = operationPathDisplay.replace(/^\//, '').replaceAll("/", " to "); + } else { + operationPathDisplay = operationPathDisplay.replace(/\//g, ''); // Remove slashes + } + operationPathDisplay = operationPathDisplay.replaceAll(" ","-"); + option.textContent = operationPathDisplay; option.value = operationPath; // Keep the value with slashes for querying group.appendChild(option); }); @@ -176,25 +232,40 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let listItem = document.createElement('li'); listItem.className = "list-group-item"; - let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && - ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || - (apiDocs[selectedOperation].post.requestBody && - apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties))); + let hasSettings = false; + if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) { + const postMethod = apiDocs[selectedOperation].post; + + // Check if parameters exist + if (postMethod.parameters && postMethod.parameters.length > 0) { + hasSettings = true; + } else if (postMethod.requestBody && postMethod.requestBody.content['multipart/form-data']) { + // Extract the reference key + const refKey = postMethod.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); + // Check if the referenced schema exists and has properties + if (apiSchemas[refKey] && Object.keys(apiSchemas[refKey].properties).length > 0) { + hasSettings = true; + } + } + } listItem.innerHTML = ` -
-
${selectedOperation}
-
- - - - -
-
- `; +
+
${selectedOperation}
+
+ + + + +
+
+`; + pipelineList.appendChild(listItem); @@ -215,23 +286,28 @@ document.getElementById('addOperationBtn').addEventListener('click', function() listItem.querySelector('.remove').addEventListener('click', function(event) { event.preventDefault(); pipelineList.removeChild(listItem); + hideOrShowPipelineHeader(); }); listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { event.preventDefault(); showpipelineSettingsModal(selectedOperation); + hideOrShowPipelineHeader(); }); function showpipelineSettingsModal(operation) { let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); let operationData = apiDocs[operation].post.parameters || []; - let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.properties || {}; + // Resolve the $ref reference to get actual schema properties + let refKey = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop(); + let requestBodyData = apiSchemas[refKey].properties || {}; + // Combine operationData and requestBodyData into a single array operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ - name: key, - schema: requestBodyData[key] + name: key, + schema: requestBodyData[key] }))); pipelineSettingsContent.innerHTML = ''; @@ -245,11 +321,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let parameterLabel = document.createElement('label'); parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; - parameterLabel.title = parameter.description; + parameterLabel.title = parameter.schema.description; + parameterLabel.setAttribute('for', parameter.name); parameterDiv.appendChild(parameterLabel); + + let defaultValue = parameter.schema.example; + if (defaultValue === undefined) defaultValue = parameter.schema.default; let parameterInput; - + // check if enum exists in schema if (parameter.schema.enum) { // if enum exists, create a select element @@ -277,11 +357,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; - parameterInput.value = "automatedFileInput"; + parameterInput.value = "FileInputPathToBeInputtedManuallyOffline"; } else { parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; } break; case 'number': @@ -289,10 +370,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'number'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; break; case 'boolean': parameterInput = document.createElement('input'); parameterInput.type = 'checkbox'; + if (defaultValue === true) parameterInput.checked = true; break; case 'array': case 'object': @@ -304,10 +387,13 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; } } parameterInput.id = parameter.name; + console.log("defaultValue", defaultValue); + console.log("parameterInput", parameterInput); if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { let savedValue = operationSettings[operation][parameter.name]; @@ -327,7 +413,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput.value = savedValue; } } - + console.log("parameterInput2", parameterInput); parameterDiv.appendChild(parameterInput); pipelineSettingsContent.appendChild(parameterDiv); @@ -340,50 +426,65 @@ document.getElementById('addOperationBtn').addEventListener('click', function() event.preventDefault(); let settings = {}; operationData.forEach(parameter => { - let value = document.getElementById(parameter.name).value; - switch (parameter.schema.type) { - case 'number': - case 'integer': - settings[parameter.name] = Number(value); - break; - case 'boolean': - settings[parameter.name] = document.getElementById(parameter.name).checked; - break; - case 'array': - case 'object': - try { - settings[parameter.name] = JSON.parse(value); - } catch (err) { - console.error(`Invalid JSON format for ${parameter.name}`); - } - break; - default: - settings[parameter.name] = value; + if(parameter.name !== "fileInput"){ + let value = document.getElementById(parameter.name).value; + switch (parameter.schema.type) { + case 'number': + case 'integer': + settings[parameter.name] = Number(value); + break; + case 'boolean': + settings[parameter.name] = document.getElementById(parameter.name).checked; + break; + case 'array': + case 'object': + try { + settings[parameter.name] = JSON.parse(value); + } catch (err) { + console.error(`Invalid JSON format for ${parameter.name}`); + } + break; + default: + settings[parameter.name] = value; + } } }); operationSettings[operation] = settings; - console.log(settings); - pipelineSettingsModal.style.display = "none"; + //pipelineSettingsModal.style.display = "none"; }); pipelineSettingsContent.appendChild(saveButton); - pipelineSettingsModal.style.display = "block"; + //pipelineSettingsModal.style.display = "block"; - pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { - pipelineSettingsModal.style.display = "none"; - } + //pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { + // pipelineSettingsModal.style.display = "none"; + //} - window.onclick = function(event) { - if (event.target == pipelineSettingsModal) { - pipelineSettingsModal.style.display = "none"; - } - } + //window.onclick = function(event) { + // if (event.target == pipelineSettingsModal) { + // pipelineSettingsModal.style.display = "none"; + // } + //} } + hideOrShowPipelineHeader(); +}); + + + + var saveBtn = document.getElementById('savePipelineBtn'); - document.getElementById('savePipelineBtn').addEventListener('click', function() { + // Remove any existing event listeners + saveBtn.removeEventListener('click', savePipeline); + + // Add the event listener + saveBtn.addEventListener('click', savePipeline); + console.log("saveBtn", saveBtn) + function savePipeline() { + if (validatePipeline() === false) { return; } + var pipelineName = document.getElementById('pipelineName').value; let pipelineList = document.getElementById('pipelineList').children; let pipelineConfig = { @@ -393,31 +494,33 @@ document.getElementById('addOperationBtn').addEventListener('click', function() "outputDir": "{outputFolder}/{folderName}", "outputFileName": "{filename}-{pipelineName}-{date}-{time}" }, - "outputDir": "httpWebRequest", + "outputDir": "{outputFolder}", "outputFileName": "{filename}" }; for (let i = 0; i < pipelineList.length; i++) { let operationName = pipelineList[i].querySelector('.operationName').textContent; let parameters = operationSettings[operationName] || {}; - + + parameters['fileInput'] = 'automated'; + pipelineConfig.pipeline.push({ "operation": operationName, "parameters": parameters }); } - + console.log("Downloading.."); let a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { type: 'application/json' })); - a.download = 'pipelineConfig.json'; + a.download = pipelineName + '.json'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); - }); + } async function processPipelineConfig(configString) { let pipelineConfig = JSON.parse(configString); @@ -483,6 +586,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function() processPipelineConfig(event.target.result); }; reader.readAsText(e.target.files[0]); + hideOrShowPipelineHeader(); }); document.getElementById('pipelineSelect').addEventListener('change', function(e) { @@ -491,4 +595,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() }); -}); \ No newline at end of file + function hideOrShowPipelineHeader() { + var pipelineHeader = document.getElementById('pipelineHeader'); + var pipelineList = document.getElementById('pipelineList'); + + if (pipelineList.children.length === 0) { + // Hide the pipeline header if there are no items in the pipeline list + pipelineHeader.style.display = 'none'; + } else { + // Show the pipeline header if there are items in the pipeline list + pipelineHeader.style.display = 'block'; + } + } diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 83303057..c0e7a757 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -306,7 +306,7 @@
diff --git a/src/main/resources/templates/convert/pdf-to-csv.html b/src/main/resources/templates/convert/pdf-to-csv.html index d0ff04c8..5cfceeb6 100644 --- a/src/main/resources/templates/convert/pdf-to-csv.html +++ b/src/main/resources/templates/convert/pdf-to-csv.html @@ -13,7 +13,7 @@

-
+
diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 31519548..2ddb3679 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -32,12 +32,12 @@
- +