mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-06-07 01:16:41 +02:00
Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
This commit is contained in:
parent
1405e4f5ee
commit
559581c59d
@ -1,18 +1,25 @@
|
|||||||
package stirling.software.SPDF.config.security.saml2;
|
package stirling.software.SPDF.config.security.saml2;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
import org.bouncycastle.util.io.pem.PemObject;
|
import org.bouncycastle.util.io.pem.PemObject;
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
import org.bouncycastle.util.io.pem.PemReader;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class CertificateUtils {
|
public class CertificateUtils {
|
||||||
|
|
||||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||||
@ -39,4 +46,84 @@ public class CertificateUtils {
|
|||||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
|
||||||
|
|
||||||
|
if (certificateResource instanceof UrlResource) {
|
||||||
|
return extractCertificateFromMetadata(certificateResource);
|
||||||
|
} else {
|
||||||
|
// Treat as file resource
|
||||||
|
return readCertificate(certificateResource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
|
||||||
|
log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
|
||||||
|
|
||||||
|
try (InputStream is = metadataResource.getInputStream()) {
|
||||||
|
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
log.info("Retrieved metadata content, length: {}", content.length());
|
||||||
|
|
||||||
|
// Find the certificate data
|
||||||
|
int startIndex = content.indexOf("<ds:X509Certificate>");
|
||||||
|
int endIndex = content.indexOf("</ds:X509Certificate>");
|
||||||
|
|
||||||
|
if (startIndex == -1 || endIndex == -1) {
|
||||||
|
log.error("Certificate tags not found in metadata");
|
||||||
|
throw new Exception("Certificate tags not found in metadata");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract certificate data
|
||||||
|
String certData = content.substring(
|
||||||
|
startIndex + "<ds:X509Certificate>".length(),
|
||||||
|
endIndex
|
||||||
|
).trim();
|
||||||
|
|
||||||
|
log.info("Found certificate data, length: {}", certData.length());
|
||||||
|
|
||||||
|
// Remove any whitespace and newlines from cert data
|
||||||
|
certData = certData.replaceAll("\\s+", "");
|
||||||
|
|
||||||
|
// Reconstruct PEM format with proper line breaks
|
||||||
|
StringBuilder pemBuilder = new StringBuilder();
|
||||||
|
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
|
||||||
|
|
||||||
|
// Insert line breaks every 64 characters
|
||||||
|
int lineLength = 64;
|
||||||
|
for (int i = 0; i < certData.length(); i += lineLength) {
|
||||||
|
int end = Math.min(i + lineLength, certData.length());
|
||||||
|
pemBuilder.append(certData, i, end).append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBuilder.append("-----END CERTIFICATE-----");
|
||||||
|
String pemCert = pemBuilder.toString();
|
||||||
|
|
||||||
|
log.debug("Reconstructed PEM certificate:\n{}", pemCert);
|
||||||
|
|
||||||
|
try {
|
||||||
|
ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
|
||||||
|
|
||||||
|
log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
|
||||||
|
|
||||||
|
// Optional: check validity dates
|
||||||
|
cert.checkValidity(); // Throws CertificateExpiredException if expired
|
||||||
|
log.info("Certificate is valid (not expired)");
|
||||||
|
|
||||||
|
return cert;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to parse certificate", e);
|
||||||
|
throw new Exception("Failed to parse X509 certificate from metadata", e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error processing metadata resource", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,17 +22,29 @@ public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthentica
|
|||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
AuthenticationException exception)
|
AuthenticationException exception)
|
||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
if (exception instanceof Saml2AuthenticationException) {
|
|
||||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
if (exception instanceof Saml2AuthenticationException saml2Exception) {
|
||||||
|
Saml2Error error = saml2Exception.getSaml2Error();
|
||||||
|
|
||||||
|
// Log detailed information about the SAML error
|
||||||
|
log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
|
||||||
|
log.error("Error description: {}", error.getDescription());
|
||||||
|
|
||||||
|
// Redirect to login with specific error code
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||||
} else if (exception instanceof ProviderNotFoundException) {
|
} else if (exception instanceof ProviderNotFoundException) {
|
||||||
|
log.error("Authentication failed: No authentication provider found");
|
||||||
|
|
||||||
getRedirectStrategy()
|
getRedirectStrategy()
|
||||||
.sendRedirect(
|
.sendRedirect(
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
"/login?erroroauth=not_authentication_provider_found");
|
"/login?erroroauth=not_authentication_provider_found");
|
||||||
}
|
} else {
|
||||||
log.error("AuthenticationException: " + exception);
|
log.error("Unknown AuthenticationException: {}", exception.getMessage());
|
||||||
|
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
package stirling.software.SPDF.config.security.saml2;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||||
|
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||||
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||||
|
|
||||||
|
public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
|
||||||
|
private final OpenSaml4AuthenticationProvider delegate;
|
||||||
|
|
||||||
|
public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
|
||||||
|
this.delegate = delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||||
|
if (authentication instanceof Saml2AuthenticationToken token) {
|
||||||
|
String samlResponse = token.getSaml2Response();
|
||||||
|
|
||||||
|
// Log the raw SAML response
|
||||||
|
log.info("Raw SAML Response (Base64): {}", samlResponse);
|
||||||
|
|
||||||
|
// Decode and log the SAML response XML
|
||||||
|
try {
|
||||||
|
String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
|
||||||
|
log.info("Decoded SAML Response XML:\n{}", decodedResponse);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// If decoding fails, it’s likely already plain XML
|
||||||
|
log.warn("SAML Response appears to be different format, not Base64-encoded.");
|
||||||
|
log.debug("SAML Response XML:\n{}", samlResponse);
|
||||||
|
}
|
||||||
|
// Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
|
||||||
|
try {
|
||||||
|
return delegate.authenticate(authentication);
|
||||||
|
} catch (Saml2AuthenticationException e) {
|
||||||
|
log.error("SAML authentication failed: {}");
|
||||||
|
log.error("Detailed error message: {}", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(Class<?> authentication) {
|
||||||
|
// Only support Saml2AuthenticationToken
|
||||||
|
return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import org.springframework.core.annotation.Order;
|
|||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
@ -30,6 +31,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
|
|||||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||||
|
import stirling.software.SPDF.utils.GeneralUtils;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConfigurationProperties(prefix = "")
|
@ConfigurationProperties(prefix = "")
|
||||||
@ -133,44 +135,20 @@ public class ApplicationProperties {
|
|||||||
private String privateKey;
|
private String privateKey;
|
||||||
private String spCert;
|
private String spCert;
|
||||||
|
|
||||||
public InputStream getIdpMetadataUri() throws IOException {
|
public Resource getIdpMetadataUri() throws IOException {
|
||||||
if (idpMetadataUri.startsWith("classpath:")) {
|
return GeneralUtils.filePathToResource(idpMetadataUri);
|
||||||
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
|
|
||||||
.getInputStream();
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
URI uri = new URI(idpMetadataUri);
|
|
||||||
URL url = uri.toURL();
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
|
||||||
connection.setRequestMethod("GET");
|
|
||||||
return connection.getInputStream();
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getSpCert() {
|
public Resource getSpCert() {
|
||||||
if (spCert.startsWith("classpath:")) {
|
return GeneralUtils.filePathToResource(spCert);
|
||||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(spCert);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getidpCert() {
|
public Resource getidpCert() {
|
||||||
if (idpCert.startsWith("classpath:")) {
|
return GeneralUtils.filePathToResource(idpCert);
|
||||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(idpCert);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Resource getPrivateKey() {
|
public Resource getPrivateKey() {
|
||||||
if (privateKey.startsWith("classpath:")) {
|
return GeneralUtils.filePathToResource(privateKey);
|
||||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
|
||||||
} else {
|
|
||||||
return new FileSystemResource(privateKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@ import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
|||||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.UrlResource;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import com.fathzer.soft.javaluator.DoubleEvaluator;
|
import com.fathzer.soft.javaluator.DoubleEvaluator;
|
||||||
@ -349,4 +353,23 @@ public class GeneralUtils {
|
|||||||
return "GenericID";
|
return "GenericID";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Resource filePathToResource(String resourceFile) {
|
||||||
|
if (resourceFile == null) {
|
||||||
|
throw new IllegalStateException("file is not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceFile.startsWith("classpath:")) {
|
||||||
|
return new ClassPathResource(resourceFile.substring("classpath:".length()));
|
||||||
|
} else if (resourceFile.startsWith("http://") || resourceFile.startsWith("https://")) {
|
||||||
|
try {
|
||||||
|
return new UrlResource(resourceFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to create URL resource: " + resourceFile, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new FileSystemResource(resourceFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -93,20 +93,71 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getPDFPageCount(file) {
|
||||||
|
try {
|
||||||
|
if (file.type !== 'application/pdf') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a URL for the file
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Ensure the worker is properly set
|
||||||
|
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the PDF document
|
||||||
|
const loadingTask = pdfjsLib.getDocument(url);
|
||||||
|
const pdf = await loadingTask.promise;
|
||||||
|
|
||||||
|
// Get the page count
|
||||||
|
const pageCount = pdf.numPages;
|
||||||
|
return pageCount;
|
||||||
|
} finally {
|
||||||
|
// Clean up the URL
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error counting PDF pages:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackFileProcessing(file, startTime, success, errorMessage = null) {
|
||||||
|
const endTime = performance.now();
|
||||||
|
const processingTime = endTime - startTime;
|
||||||
|
|
||||||
|
posthog.capture('file_processing', {
|
||||||
|
success: success,
|
||||||
|
file_type: file.type || 'unknown',
|
||||||
|
file_size: file.size,
|
||||||
|
processing_time: processingTime,
|
||||||
|
error_message: errorMessage,
|
||||||
|
pdf_pages: file.type === 'application/pdf' ? getPDFPageCount(file) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
|
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const file = formData.get('fileInput');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, { method: "POST", body: formData });
|
const response = await fetch(url, { method: "POST", body: formData });
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// Handle 401 Unauthorized error
|
|
||||||
showSessionExpiredPrompt();
|
showSessionExpiredPrompt();
|
||||||
|
trackFileProcessing(file, startTime, false, 'unauthorized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (contentType && contentType.includes("application/json")) {
|
if (contentType && contentType.includes("application/json")) {
|
||||||
console.error("Throwing error banner, response was not okay");
|
console.error("Throwing error banner, response was not okay");
|
||||||
return handleJsonResponse(response);
|
const jsonResponse = await handleJsonResponse(response);
|
||||||
|
trackFileProcessing(file, startTime, false, jsonResponse?.error || 'unknown_error');
|
||||||
|
return jsonResponse;
|
||||||
}
|
}
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
@ -115,13 +166,15 @@
|
|||||||
let filename = getFilenameFromContentDisposition(contentDisposition);
|
let filename = getFilenameFromContentDisposition(contentDisposition);
|
||||||
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
if (contentType.includes("application/pdf") || contentType.includes("image/")) {
|
const result = await handleResponse(blob, filename, !isMulti, isZip);
|
||||||
return handleResponse(blob, filename, !isMulti, isZip);
|
|
||||||
} else {
|
// Track successful processing
|
||||||
return handleResponse(blob, filename, false, isZip);
|
trackFileProcessing(file, startTime, true, null);
|
||||||
}
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in handleSingleDownload:", error);
|
console.error("Error in handleSingleDownload:", error);
|
||||||
|
trackFileProcessing(file, startTime, false, error.message);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -162,8 +215,7 @@
|
|||||||
if (considerViewOptions) {
|
if (considerViewOptions) {
|
||||||
if (downloadOption === "sameWindow") {
|
if (downloadOption === "sameWindow") {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
window.location.href = url;
|
return { filename, blob, url };
|
||||||
return;
|
|
||||||
} else if (downloadOption === "newWindow") {
|
} else if (downloadOption === "newWindow") {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
|
@ -76,7 +76,7 @@ async function checkForUpdate() {
|
|||||||
document.getElementById("update-btn").style.display = "block";
|
document.getElementById("update-btn").style.display = "block";
|
||||||
}
|
}
|
||||||
if (updateLink !== null) {
|
if (updateLink !== null) {
|
||||||
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
|
document.getElementById("app-update").innerText = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
|
||||||
if (updateLink.classList.contains("visually-hidden")) {
|
if (updateLink.classList.contains("visually-hidden")) {
|
||||||
updateLink.classList.remove("visually-hidden");
|
updateLink.classList.remove("visually-hidden");
|
||||||
}
|
}
|
||||||
|
@ -201,6 +201,7 @@
|
|||||||
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
|
window.stirlingPDF.error = /*[[#{error}]]*/ "Error";
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
|
||||||
<script th:src="@{'/js/downloader.js'}"></script>
|
<script th:src="@{'/js/downloader.js'}"></script>
|
||||||
|
|
||||||
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
|
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
|
||||||
|
Loading…
Reference in New Issue
Block a user