From 4436759e126615973973b74c3c91f7a87fb10703 Mon Sep 17 00:00:00 2001 From: a Date: Fri, 20 Sep 2024 13:31:55 +0100 Subject: [PATCH] #1869 and ensure naming Signed-off-by: a --- .../SPDF/EE/KeygenLicenseVerifier.java | 182 ++++++++++++++++++ .../software/SPDF/EE/LicenseKeyChecker.java | 58 ++++++ .../software/SPDF/config/MetricsFilter.java | 6 +- .../config/security/FirstLoginFilter.java | 19 ++ .../security/UserAuthenticationFilter.java | 31 ++- .../session/CustomHttpSessionListener.java | 24 ++- .../session/SessionPersistentRegistry.java | 8 + .../controller/web/GeneralWebController.java | 7 + src/main/resources/application.properties | 1 + src/main/resources/messages_en_GB.properties | 23 ++- src/main/resources/static/js/downloader.js | 23 ++- .../resources/templates/fragments/common.html | 1 + src/main/resources/templates/home.html | 48 +++-- .../templates/split-pdf-by-chapters.html | 64 ++++++ 14 files changed, 467 insertions(+), 28 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java create mode 100644 src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java create mode 100644 src/main/resources/templates/split-pdf-by-chapters.html diff --git a/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java new file mode 100644 index 000000000..564388aa6 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/EE/KeygenLicenseVerifier.java @@ -0,0 +1,182 @@ +package stirling.software.SPDF.EE; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +import org.springframework.stereotype.Service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class KeygenLicenseVerifier { + private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372"; + private static final String PRODUCT_ID = "f9bb2423-62c9-4d39-8def-4fdc5aca751e"; + private static final String BASE_URL = "https://api.keygen.sh/v1/accounts"; + private static final String PUBLIC_KEY = + "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzJaf7jPx/bamT/ctmvrf\n" + + "5HfzV9CrTx39Hv48NvRIjw9jBAlmcSndLbgcrTUWFrd7pJPPEhzmfJ9tLRg0a3Si\n" + + "34Ed9gQ24mODj0Wpos5uwwxu1M5wzsKPjkLZDigB3d9L/79nyKvSUo+mx+dZmZnD\n" + + "D19TMM93ZDxG+Bru5/rvvxaZzMHZAnqrTdoO55vFjpss5XJNt6kz4jxr+D6a3lFU\n" + + "GGCx7bjeanHCNGRw84dLYbU8s5DGsx5JNX1xPGR1kODocvsHfHJvsxfdNtpH4vke\n" + + "yOrtEUCp01Mh2kr3zM8R4Yjh4ae2qHiZne0FiVhiUaHmbf2dmcA9O1Kynz33634s\n" + + "fwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static boolean verifyLicense(String licenseKey) { + try { + String machineFingerprint = generateMachineFingerprint(); + + // First, try to validate the license + boolean isValid = validateLicense(licenseKey, machineFingerprint); + + // If validation fails, try to activate the machine + if (!isValid) { + System.out.println( + "License validation failed. Attempting to activate the machine..."); + isValid = activateMachine(licenseKey, machineFingerprint); + + if (isValid) { + // If activation is successful, try to validate again + isValid = validateLicense(licenseKey, machineFingerprint); + } + } + + return isValid; + } catch (Exception e) { + System.out.println("Error verifying license: " + e.getMessage()); + return false; + } + } + + private static boolean validateLicense(String licenseKey, String machineFingerprint) + throws Exception { + HttpClient client = HttpClient.newHttpClient(); + String requestBody = + String.format( + "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\",\"product\":\"%s\"}}}", + licenseKey, machineFingerprint, PRODUCT_ID); + + HttpRequest request = + HttpRequest.newBuilder() + .uri( + URI.create( + BASE_URL + + "/" + + ACCOUNT_ID + + "/licenses/actions/validate-key")) + .header("Content-Type", "application/vnd.api+json") + .header("Accept", "application/vnd.api+json") + .header("Authorization", "license " + licenseKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200) { + JsonNode jsonResponse = objectMapper.readTree(response.body()); + JsonNode metaNode = jsonResponse.path("meta"); + boolean isValid = metaNode.path("valid").asBoolean(); + String detail = metaNode.path("detail").asText(); + String code = metaNode.path("code").asText(); + + System.out.println("License validity: " + isValid); + System.out.println("Validation detail: " + detail); + System.out.println("Validation code: " + code); + + if (isValid) { + return verifySignature(metaNode); + } + } else { + System.out.println("Error validating license. Status code: " + response.statusCode()); + System.out.println("Response body: " + response.body()); + } + return false; + } + + private static boolean activateMachine(String licenseKey, String machineFingerprint) + throws Exception { + HttpClient client = HttpClient.newHttpClient(); + String requestBody = + String.format( + "{\"data\":{\"type\":\"machines\",\"attributes\":{\"fingerprint\":\"%s\"},\"relationships\":{\"license\":{\"data\":{\"type\":\"licenses\",\"id\":\"%s\"}}}}}", + machineFingerprint, licenseKey); + + String licenseId = "8e072b67-3cea-454b-98bb-bb73bbc04bd4"; + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines")) + .header("Content-Type", "application/vnd.api+json") + .header("Accept", "application/vnd.api+json") + .header("Authorization", "license " + licenseId) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 201) { + System.out.println("Machine activated successfully"); + return true; + } else { + System.out.println("Error activating machine. Status code: " + response.statusCode()); + System.out.println("Response body: " + response.body()); + return false; + } + } + + private static boolean verifySignature(JsonNode metaNode) throws Exception { + String signature = metaNode.path("signature").asText(); + String data = metaNode.path("data").asText(); + + PublicKey publicKey = + KeyFactory.getInstance("RSA") + .generatePublic( + new X509EncodedKeySpec( + Base64.getDecoder() + .decode( + PUBLIC_KEY + .replace( + "-----BEGIN PUBLIC KEY-----", + "") + .replace( + "-----END PUBLIC KEY-----", + "") + .replaceAll("\\s", "")))); + + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initVerify(publicKey); + sig.update(data.getBytes()); + + boolean isSignatureValid = sig.verify(Base64.getDecoder().decode(signature)); + System.out.println("Signature validity: " + isSignatureValid); + return isSignatureValid; + } + + private static String generateMachineFingerprint() { + // This is a simplified example. In a real-world scenario, you'd want to generate + // a more robust and unique fingerprint based on hardware characteristics. + return "example-fingerprint-" + System.currentTimeMillis(); + } + + public static void test() { + String[] testKeys = { + "FYKJ-YK7F-MEVX-RYKK-JYWE-77WW-3TKN-PJRU", "EFDB57-92B4C2-EDFA20-51146E-E1AF4A-V3" + }; + + for (String licenseKey : testKeys) { + System.out.println("Testing license key: " + licenseKey); + boolean isValid = verifyLicense(licenseKey); + System.out.println("License is valid: " + isValid); + System.out.println("--------------------"); + } + } +} diff --git a/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java new file mode 100644 index 000000000..7ca54e8c8 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/EE/LicenseKeyChecker.java @@ -0,0 +1,58 @@ +package stirling.software.SPDF.EE; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import stirling.software.SPDF.model.ApplicationProperties; + +@Component +public class LicenseKeyChecker implements CommandLineRunner { + + private final KeygenLicenseVerifier licenseService; + + private final ApplicationProperties applicationProperties; + + // Inject your license service or configuration + public LicenseKeyChecker( + KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { + this.licenseService = licenseService; + this.applicationProperties = new ApplicationProperties(); + } + + // Validate on startup + @Override + public void run(String... args) throws Exception { + checkLicense(); + } + + // Periodic license check - runs every 7 days + @Scheduled(fixedRate = 604800000) // 7 days in milliseconds + public void checkLicensePeriodically() { + checkLicense(); + } + + // License validation logic + private void checkLicense() { + boolean isValid = + licenseService.verifyLicense(applicationProperties.getEnterpriseEdition().getKey()); + if (!isValid) { + // Handle invalid license (shut down the app, log, etc.) + System.out.println("License key is invalid!"); + // Optionally stop the application + // System.exit(1); // Uncomment if you want to stop the app + } else { + System.out.println("License key is valid."); + } + } + + // Method to update the license key dynamically + public void updateLicenseKey(String newKey) { + // Update the key in ApplicationProperties + applicationProperties.getEnterpriseEdition().setKey(newKey); + + // Immediately validate the new key + System.out.println("License key has been updated. Checking new key..."); + checkLicense(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index c0231ca7f..fba1ee9ce 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -13,6 +13,7 @@ import jakarta.servlet.FilterChain; 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 @@ -32,10 +33,11 @@ public class MetricsFilter extends OncePerRequestFilter { String uri = request.getRequestURI(); if (RequestUriUtils.isTrackableResource(request.getContextPath(), uri)) { - + HttpSession session = request.getSession(false); + String sessionId = (session != null) ? session.getId() : "no-session"; Counter counter = Counter.builder("http.requests") - .tag("session", request.getSession().getId()) + .tag("session", sessionId) .tag("method", request.getMethod()) .tag("uri", uri) .register(meterRegistry); 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 86afa0287..72c146e65 100644 --- a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -1,6 +1,8 @@ package stirling.software.SPDF.config.security; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Optional; import org.springframework.beans.factory.annotation.Autowired; @@ -14,9 +16,12 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.User; import stirling.software.SPDF.utils.RequestUriUtils; +@Slf4j @Component public class FirstLoginFilter extends OncePerRequestFilter { @@ -50,6 +55,20 @@ public class FirstLoginFilter extends OncePerRequestFilter { return; } } + + HttpSession session = request.getSession(true); + SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); + String creationTime = timeFormat.format(new Date(session.getCreationTime())); + + log.info( + "Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}", + session.isNew(), + creationTime, + session.getId(), + request.getRemoteAddr(), + request.getHeader("User-Agent"), + request.getHeader("Referer"), + request.getRequestURL().toString()); filterChain.doFilter(request, response); } } 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 79b285211..68e4cc1c5 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -5,7 +5,6 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpStatus; @@ -30,13 +29,18 @@ import stirling.software.SPDF.model.User; @Component public class UserAuthenticationFilter extends OncePerRequestFilter { - @Autowired @Lazy private UserService userService; + private final UserService userService; + private final SessionPersistentRegistry sessionPersistentRegistry; + private final boolean loginEnabledValue; - @Autowired private SessionPersistentRegistry sessionPersistentRegistry; - - @Autowired - @Qualifier("loginEnabled") - public boolean loginEnabledValue; + public UserAuthenticationFilter( + @Lazy UserService userService, + SessionPersistentRegistry sessionPersistentRegistry, + @Qualifier("loginEnabled") boolean loginEnabledValue) { + this.userService = userService; + this.sessionPersistentRegistry = sessionPersistentRegistry; + this.loginEnabledValue = loginEnabledValue; + } @Override protected void doFilterInternal( @@ -51,6 +55,19 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { String requestURI = request.getRequestURI(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // Check for session expiration (unsure if needed) +// if (authentication != null && authentication.isAuthenticated()) { +// String sessionId = request.getSession().getId(); +// SessionInformation sessionInfo = +// sessionPersistentRegistry.getSessionInformation(sessionId); +// +// if (sessionInfo != null && sessionInfo.isExpired()) { +// SecurityContextHolder.clearContext(); +// response.sendRedirect(request.getContextPath() + "/login?expired=true"); +// return; +// } +// } + // Check for API key in the request headers if no authentication exists if (authentication == null || !authentication.isAuthenticated()) { String apiKey = request.getHeader("X-API-Key"); diff --git a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java index a4129e6a3..a6a9b5883 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/CustomHttpSessionListener.java @@ -1,5 +1,7 @@ package stirling.software.SPDF.config.security.session; +import java.util.concurrent.atomic.AtomicInteger; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -11,16 +13,32 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CustomHttpSessionListener implements HttpSessionListener { - @Autowired private SessionPersistentRegistry sessionPersistentRegistry; + private SessionPersistentRegistry sessionPersistentRegistry; + + private final AtomicInteger activeSessions; + + @Autowired + public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) { + super(); + this.sessionPersistentRegistry = sessionPersistentRegistry; + activeSessions = new AtomicInteger(); + } @Override public void sessionCreated(HttpSessionEvent se) { - log.info("Session created: " + se.getSession().getId()); + log.info( + "Session created: {} with count {}", + se.getSession().getId(), + activeSessions.incrementAndGet()); + } @Override public void sessionDestroyed(HttpSessionEvent se) { - log.info("Session destroyed: " + se.getSession().getId()); + log.info( + "Session destroyed: {} with count {}", + se.getSession().getId(), + activeSessions.decrementAndGet()); sessionPersistentRegistry.expireSession(se.getSession().getId()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java index bb63a8b5e..615a2827f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java +++ b/src/main/java/stirling/software/SPDF/config/security/session/SessionPersistentRegistry.java @@ -84,6 +84,14 @@ public class SessionPersistentRegistry implements SessionRegistry { } if (principalName != null) { + // Clear old sessions for the principal (unsure if needed) +// List existingSessions = +// sessionRepository.findByPrincipalName(principalName); +// for (SessionEntity session : existingSessions) { +// session.setExpired(true); +// sessionRepository.save(session); +// } + SessionEntity sessionEntity = new SessionEntity(); sessionEntity.setSessionId(sessionId); sessionEntity.setPrincipalName(principalName); 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 93ad5f342..14af6ee7a 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -108,6 +108,13 @@ public class GeneralWebController { return "split-pdf-by-sections"; } + @GetMapping("/split-pdf-by-chapters") + @Hidden + public String splitPdfByChapters(Model model) { + model.addAttribute("currentPage", "split-pdf-by-chapters"); + return "split-pdf-by-chapters"; + } + @GetMapping("/view-pdf") @Hidden public String ViewPdfForm2(Model model) { diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cae1dce3d..cbe67a3e7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -30,6 +30,7 @@ spring.devtools.livereload.enabled=true spring.thymeleaf.encoding=UTF-8 +spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 206fae045..51f91898e 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -223,6 +223,8 @@ database.fileNotFound=File not found database.fileNullOrEmpty=File must not be null or empty database.failedImportFile=Failed to import file +session.expired=Your session has expired. Please refresh the page and try again. + ############# # HOME-PAGE # ############# @@ -480,6 +482,9 @@ home.removeImagePdf.title=Remove image home.removeImagePdf.desc=Remove image from PDF to reduce file size removeImagePdf.tags=Remove Image,Page operations,Back end,server side +home.splitPdfByChapters.title=Split PDF by Chapters +home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. +splitPdfByChapters.tags=split,chapters,bookmarks,organize ########################### # # @@ -1132,7 +1137,9 @@ licenses.license=Licence survey.nav=Survey survey.title=Stirling-PDF Survey survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF! -survey.please=Please consider taking our survey! +survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here: +survey.changes2=With these changes we are getting paid business support and funding +survey.please=Please consider taking our survey to have input on the future of Stirling-PDF! survey.disabled=(Survey popup will be disabled in following updates but available at foot of page) survey.button=Take Survey survey.dontShowAgain=Don't show again @@ -1157,3 +1164,17 @@ removeImage.title=Remove image removeImage.header=Remove image removeImage.removeImage=Remove image removeImage.submit=Remove image + + +splitByChapters.title=Split PDF by Chapters +splitByChapters.header=Split PDF by Chapters +splitByChapters.bookmarkLevel=Bookmark Level +splitByChapters.includeMetadata=Include Metadata +splitByChapters.allowDuplicates=Allow Duplicates +splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure. +splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.). +splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. +splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. +splitByChapters.submit=Split PDF + + diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js index 46e6a9483..ba5edfc31 100644 --- a/src/main/resources/static/js/downloader.js +++ b/src/main/resources/static/js/downloader.js @@ -5,6 +5,22 @@ function showErrorBanner(message, stackTrace) { document.querySelector("#errorContainer p").textContent = message; document.querySelector("#traceContent").textContent = stackTrace; } + +function showSessionExpiredPrompt() { + const errorContainer = document.getElementById("errorContainer"); + errorContainer.style.display = "block"; + document.querySelector("#errorContainer .alert-heading").textContent = "Session Expired"; + document.querySelector("#errorContainer p").textContent = sessionExpired; + document.querySelector("#traceContent").textContent = ""; + + // Optional: Add a refresh button + const refreshButton = document.createElement("button"); + refreshButton.textContent = "Refresh Page"; + refreshButton.className = "btn btn-primary mt-3"; + refreshButton.onclick = () => location.reload(); + errorContainer.appendChild(refreshButton); +} + let firstErrorOccurred = false; $(document).ready(function () { @@ -79,6 +95,11 @@ async function handleSingleDownload(url, formData, isMulti = false, isZip = fals const contentType = response.headers.get("content-type"); if (!response.ok) { + if (response.status === 401) { + // Handle 401 Unauthorized error + showSessionExpiredPrompt(); + return; + } if (contentType && contentType.includes("application/json")) { console.error("Throwing error banner, response was not okay"); return handleJsonResponse(response); @@ -97,7 +118,7 @@ async function handleSingleDownload(url, formData, isMulti = false, isZip = fals } } catch (error) { console.error("Error in handleSingleDownload:", error); - throw error; // Re-throw the error if you want it to be handled higher up. + throw error; } } diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index 700c68fd2..661e25e60 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -148,6 +148,7 @@ const multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false; const disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false; const remoteCall = /*[[${remoteCall}]]*/ true; + const sessionExpired = /*[[#{session.expired}]]*/ ''; diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 30c295a51..46189ad06 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -7,9 +7,11 @@ +
+
- +
@@ -232,6 +234,9 @@
+
+
@@ -248,7 +253,9 @@
diff --git a/src/main/resources/templates/split-pdf-by-chapters.html b/src/main/resources/templates/split-pdf-by-chapters.html new file mode 100644 index 000000000..73ec9d562 --- /dev/null +++ b/src/main/resources/templates/split-pdf-by-chapters.html @@ -0,0 +1,64 @@ + + + + + + + + +
+
+ +

+
+
+
+
+ book + +
+
+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +

+ +

+
+

+

+

+

+
+ +
+ +
+ +
+
+
+
+ +
+ + + \ No newline at end of file