mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-06 18:30:57 +00:00
parent
87925ac618
commit
4436759e12
@ -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<String> 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<String> 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("--------------------");
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,14 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
}
|
||||
|
||||
if (principalName != null) {
|
||||
// Clear old sessions for the principal (unsure if needed)
|
||||
// List<SessionEntity> existingSessions =
|
||||
// sessionRepository.findByPrincipalName(principalName);
|
||||
// for (SessionEntity session : existingSessions) {
|
||||
// session.setExpired(true);
|
||||
// sessionRepository.save(session);
|
||||
// }
|
||||
|
||||
SessionEntity sessionEntity = new SessionEntity();
|
||||
sessionEntity.setSessionId(sessionId);
|
||||
sessionEntity.setPrincipalName(principalName);
|
||||
|
@ -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) {
|
||||
|
@ -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/
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,6 +148,7 @@
|
||||
const multipleInputsForSingleRequest = /*[[${multipleInputsForSingleRequest}]]*/ false;
|
||||
const disableMultipleFiles = /*[[${disableMultipleFiles}]]*/ false;
|
||||
const remoteCall = /*[[${remoteCall}]]*/ true;
|
||||
const sessionExpired = /*[[#{session.expired}]]*/ '';
|
||||
</script>
|
||||
<script th:src="@{'/js/downloader.js'}"></script>
|
||||
|
||||
|
@ -7,9 +7,11 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<div id="page-container">
|
||||
|
||||
<div id="content-wrap">
|
||||
|
||||
<!-- Jumbotron -->
|
||||
<div class="p-5 rounded d-none d-md-block" id="jumbotron">
|
||||
<div class="container">
|
||||
@ -232,6 +234,9 @@
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
|
||||
</div>
|
||||
<div
|
||||
th:replace="~{fragments/card :: card(id='split-pdf-by-chapters', cardTitle=#{home.splitPdfByChapters.title}, cardText=#{home.splitPdfByChapters.desc}, cardLink='split-pdf-by-chapters', toolIcon='book', tags=#{splitPdfByChapters.tags}, toolGroup='organize')}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -248,7 +253,9 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p th:text="#{survey.description}">Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF!</h5>
|
||||
<p th:text="#{survey.changes}">Stirling-PDF has changed since the last survey! To find out more please check our blog post here:</h5>
|
||||
<a href="https://stirlingpdf.info/blog/stirling-pdf-survey-results" target="_blank" th:text="#{survey.changes2}">https://stirlingpdf.info/blog/stirling-pdf-survey-results</a>
|
||||
<p th:text="#{survey.changes2}">With these changes we are getting paid business support and funding</p>
|
||||
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||
<p th:text="#{survey.disabled}">Survey popup will be disabled in following updates but available at foot of page)</p>
|
||||
<a href="https://stirlingpdf.info/s/clwzgtfw7000gltkmwz1n212m" target="_blank" class="btn btn-primary" id="takeSurvey"th:text="#{survey.button}" >Take Survey</a>
|
||||
@ -267,17 +274,34 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/*
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const surveyVersion = "1.1";
|
||||
const surveyVersion = "2.0";
|
||||
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion || !localStorage.getItem('dontShowSurvey')) {
|
||||
modal.show();
|
||||
const viewThresholds = [5, 15, 30, 50, 75, 100, 150, 200];
|
||||
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||
|
||||
pageViews++;
|
||||
localStorage.setItem('pageViews', pageViews.toString());
|
||||
|
||||
function shouldShowSurvey() {
|
||||
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('surveyVersion') !== surveyVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return viewThresholds.includes(pageViews);
|
||||
}
|
||||
|
||||
if (shouldShowSurvey()) {
|
||||
modal.show();
|
||||
}
|
||||
|
||||
dontShowAgain.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
@ -290,15 +314,11 @@
|
||||
});
|
||||
|
||||
takeSurveyButton.addEventListener('click', function() {
|
||||
localStorage.setItem('dontShowSurvey', 'true');
|
||||
localStorage.setItem('surveyTaken', 'true');
|
||||
localStorage.setItem('surveyVersion', surveyVersion);
|
||||
modal.hide();
|
||||
});
|
||||
|
||||
if (localStorage.getItem('dontShowSurvey')) {
|
||||
modal.hide();
|
||||
}
|
||||
});*/
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
64
src/main/resources/templates/split-pdf-by-chapters.html
Normal file
64
src/main/resources/templates/split-pdf-by-chapters.html
Normal file
@ -0,0 +1,64 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{splitByChapters.title}, header=#{splitByChapters.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">book</span>
|
||||
<span class="tool-header-text" th:text="#{splitByChapters.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/general/split-pdf-by-chapters'}" method="post" enctype="multipart/form-data">
|
||||
<div
|
||||
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="bookmarkLevel" th:text="#{splitByChapters.bookmarkLevel}"></label>
|
||||
<input type="number" class="form-control" id="bookmarkLevel" name="bookmarkLevel" min="0" value="0" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="includeMetadata" name="includeMetadata">
|
||||
<label class="form-check-label" for="includeMetadata" th:text="#{splitByChapters.includeMetadata}"></label>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="allowDuplicates" name="allowDuplicates">
|
||||
<label class="form-check-label" for="allowDuplicates" th:text="#{splitByChapters.allowDuplicates}"></label>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a class="btn btn-outline-primary" data-bs-toggle="collapse" href="#info" role="button"
|
||||
aria-expanded="false" aria-controls="info" th:text="#{info}"></a>
|
||||
</p>
|
||||
<div class="collapse" id="info">
|
||||
<p th:text="#{splitByChapters.desc.1}"></p>
|
||||
<p th:text="#{splitByChapters.desc.2}"></p>
|
||||
<p th:text="#{splitByChapters.desc.3}"></p>
|
||||
<p th:text="#{splitByChapters.desc.4}"></p>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{splitByChapters.submit}"></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user