1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-09-29 16:10:11 +02:00

Merge pull request #322 from Frooodle/bootstrap5_test

Bootstrap5
This commit is contained in:
Anthony Stirling 2023-08-26 22:35:59 +01:00 committed by GitHub
commit 09a0779180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
95 changed files with 1906 additions and 520 deletions

2
.gitignore vendored
View File

@ -19,7 +19,7 @@ pipeline/
#### Stirling-PDF Files ### #### Stirling-PDF Files ###
customFiles/ customFiles/
config/ configs/
watchedFolders/ watchedFolders/

View File

@ -208,7 +208,11 @@ For API usage you must provide a header with 'X-API-Key' and the associated API
- Progress bar/Tracking - Progress bar/Tracking
- Full custom logic pipelines to combine multiple operations together. - Full custom logic pipelines to combine multiple operations together.
- Folder support with auto scanning to perform operations on - Folder support with auto scanning to perform operations on
- Redact sections of pages - Redact text (Via UI)
- Add Forms
- Annotations
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
- Fill forms mannual and automatic
### Q2: Why is my application downloading .htm files? ### Q2: Why is my application downloading .htm files?
This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files. This is a issue caused commonly by your NGINX congifuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. client_max_body_size SIZE; Where "SIZE" is 50M for example for 50MB files.

View File

@ -4,6 +4,7 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Collections;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@ -14,6 +15,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.config.ConfigInitializer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication @SpringBootApplication
@EnableWebSecurity() @EnableWebSecurity()
@ -51,7 +53,15 @@ public class SPdfApplication {
} }
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(SPdfApplication.class, args); SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) {
app.setDefaultProperties(Collections.singletonMap("spring.config.location", "file:configs/settings.yml"));
} else {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
}
app.run(args);
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {

View File

@ -1,37 +1,30 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import stirling.software.SPDF.utils.PropertyConfigs;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class AppConfig { public class AppConfig {
@Autowired
ApplicationProperties applicationProperties;
@Bean(name = "rateLimit")
public boolean rateLimit() { @Bean(name = "loginEnabled")
String appName = System.getProperty("rateLimit");
if (appName == null)
appName = System.getenv("rateLimit");
System.out.println("rateLimit=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "loginEnabled")
public boolean loginEnabled() { public boolean loginEnabled() {
String appName = System.getProperty("login.enabled"); System.out.println(applicationProperties.toString());
if (appName == null) return applicationProperties.getSecurity().getEnableLogin();
appName = System.getenv("login.enabled");
System.out.println("loginEnabled=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false;
} }
@Bean(name = "appName") @Bean(name = "appName")
public String appName() { public String appName() {
String appName = System.getProperty("APP_HOME_NAME"); return applicationProperties.getUi().getHomeName();
if (appName == null)
appName = System.getenv("APP_HOME_NAME");
return (appName != null) ? appName : "Stirling PDF";
} }
@Bean(name = "appVersion") @Bean(name = "appVersion")
@ -42,22 +35,24 @@ public class AppConfig {
@Bean(name = "homeText") @Bean(name = "homeText")
public String homeText() { public String homeText() {
String homeText = System.getProperty("APP_HOME_DESCRIPTION"); return applicationProperties.getUi().getHomeDescription();
if (homeText == null)
homeText = System.getenv("APP_HOME_DESCRIPTION");
return (homeText != null) ? homeText : "null";
} }
@Bean(name = "navBarText") @Bean(name = "navBarText")
public String navBarText() { public String navBarText() {
String navBarText = System.getProperty("APP_NAVBAR_NAME"); String defaultNavBar = applicationProperties.getUi().getNavbarName() != null ? applicationProperties.getUi().getNavbarName() : applicationProperties.getUi().getHomeName();
if (navBarText == null) return defaultNavBar;
navBarText = System.getenv("APP_NAVBAR_NAME");
if (navBarText == null)
navBarText = System.getProperty("APP_HOME_NAME");
if (navBarText == null)
navBarText = System.getenv("APP_HOME_NAME");
return (navBarText != null) ? navBarText : "Stirling PDF";
} }
@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;
}
} }

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.config;
import java.time.Duration; import java.time.Duration;
import java.util.Locale; import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
@ -15,10 +16,14 @@ import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j; import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill; import io.github.bucket4j.Refill;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Autowired
ApplicationProperties applicationProperties;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
@ -35,10 +40,9 @@ public class Beans implements WebMvcConfigurer {
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = System.getProperty("APP_LOCALE");
if (appLocaleEnv == null) String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
appLocaleEnv = System.getenv("APP_LOCALE");
Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {

View File

@ -24,7 +24,7 @@ import org.springframework.web.servlet.ModelAndView;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error"); private static final List<String> ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file");
@Override @Override
@ -40,11 +40,9 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
String[] queryParameters = queryString.split("&"); String[] queryParameters = queryString.split("&");
for (String param : queryParameters) { for (String param : queryParameters) {
String[] keyValue = param.split("="); String[] keyValue = param.split("=");
System.out.print("astirli " + keyValue[0]);
if (keyValue.length != 2) { if (keyValue.length != 2) {
continue; continue;
} }
System.out.print("astirli2 " + keyValue[0]);
if (ALLOWED_PARAMS.contains(keyValue[0])) { if (ALLOWED_PARAMS.contains(keyValue[0])) {
parameters.put(keyValue[0], keyValue[1]); parameters.put(keyValue[0], keyValue[1]);

View File

@ -0,0 +1,43 @@
package stirling.software.SPDF.config;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class ConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
ensureConfigExists();
} catch (IOException e) {
throw new RuntimeException("Failed to initialize application configuration", e);
}
}
public void ensureConfigExists() throws IOException {
// Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists
if (Files.notExists(destPath)) {
// Ensure the destination directory exists
Files.createDirectories(destPath.getParent());
// Copy the resource from classpath to the external directory
try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
if (in != null) {
Files.copy(in, destPath);
} else {
throw new FileNotFoundException("Resource file not found: settings.yml.template");
}
}
}
}
}

View File

@ -1,20 +1,28 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
public EndpointConfiguration() { private final ApplicationProperties applicationProperties;
@Autowired
public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
init(); init();
processEnvironmentConfigs(); processEnvironmentConfigs();
} }
@ -198,21 +206,19 @@ public class EndpointConfiguration {
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
String endpointsToRemove = System.getenv("ENDPOINTS_TO_REMOVE"); List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
String groupsToRemove = System.getenv("GROUPS_TO_REMOVE"); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (endpointsToRemove != null) { if (endpointsToRemove != null) {
String[] endpoints = endpointsToRemove.split(","); for (String endpoint : endpointsToRemove) {
for (String endpoint : endpoints) {
disableEndpoint(endpoint.trim()); disableEndpoint(endpoint.trim());
} }
} }
if (groupsToRemove != null) { if (groupsToRemove != null) {
String[] groups = groupsToRemove.split(","); for (String group : groupsToRemove) {
for (String group : groups) {
disableGroup(group.trim()); disableGroup(group.trim());
} }
} }

View File

@ -0,0 +1,23 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.Properties;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource encodedResource)
throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties);
}
}

View File

@ -1,26 +1,82 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
@Component @Component
public class InitialSetup { public class InitialSetup {
@Autowired @Autowired
private UserService userService; private UserService userService;
@PostConstruct @Autowired
public void init() { ApplicationProperties applicationProperties;
if(!userService.hasUsers()) {
String initialUsername = System.getenv("INITIAL_USERNAME"); @PostConstruct
String initialPassword = System.getenv("INITIAL_PASSWORD"); public void init() {
if(initialUsername != null && initialPassword != null) { if (!userService.hasUsers()) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername();
} String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null && initialPassword != null) {
} userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
} }
}
}
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
}
}
private void saveKeyToConfig(String key) throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
List<String> lines = Files.readAllLines(path);
boolean keyFound = false;
// Search for the existing key to replace it or place to add it
for (int i = 0; i < lines.size(); i++) {
if (lines.get(i).startsWith("AutomaticallyGenerated:")) {
keyFound = true;
if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) {
lines.set(i + 1, " key: " + key);
break;
} else {
lines.add(i + 1, " key: " + key);
break;
}
}
}
// If the section doesn't exist, append it
if (!keyFound) {
lines.add("# Automatically Generated Settings (Do Not Edit Directly)");
lines.add("AutomaticallyGenerated:");
lines.add(" key: " + key);
}
// Write back to the file
Files.write(path, lines);
}
}

View File

@ -15,7 +15,11 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@Configuration @Configuration
public class SecurityConfiguration { public class SecurityConfiguration {
@ -43,7 +47,7 @@ public class SecurityConfiguration {
if(loginEnabledValue) { if(loginEnabledValue) {
http.csrf().disable(); http.csrf(csrf -> csrf.disable());
http http
.formLogin(formLogin -> formLogin .formLogin(formLogin -> formLogin
.loginPage("/login") .loginPage("/login")
@ -55,8 +59,12 @@ public class SecurityConfiguration {
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true") .logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session .invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID") .deleteCookies("JSESSIONID", "remember-me")
) ).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(authz -> authz .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/")) .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() .permitAll()
@ -65,8 +73,7 @@ public class SecurityConfiguration {
.userDetailsService(userDetailsService) .userDetailsService(userDetailsService)
.authenticationProvider(authenticationProvider()); .authenticationProvider(authenticationProvider());
} else { } else {
http http.csrf(csrf -> csrf.disable())
.csrf().disable()
.authorizeHttpRequests(authz -> authz .authorizeHttpRequests(authz -> authz
.anyRequest().permitAll() .anyRequest().permitAll()
); );
@ -84,7 +91,12 @@ public class SecurityConfiguration {
return authProvider; return authProvider;
} }
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
} }

View File

@ -47,6 +47,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; 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.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -91,6 +92,10 @@ public class PipelineController {
} }
} }
@Autowired
ApplicationProperties applicationProperties;
private void handleDirectory(Path dir) throws Exception { private void handleDirectory(Path dir) throws Exception {
logger.info("Handling directory: {}", dir); logger.info("Handling directory: {}", dir);
Path jsonFile = dir.resolve(jsonFileName); Path jsonFile = dir.resolve(jsonFileName);
@ -182,8 +187,7 @@ public class PipelineController {
// {filename} {folder} {date} {tmime} {pipeline} // {filename} {folder} {date} {tmime} {pipeline}
String outputDir = config.getOutputDir(); String outputDir = config.getOutputDir();
// Check if the environment variable 'automatedOutputFolder' is set String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder();
String outputFolder = System.getenv("automatedOutputFolder");
if (outputFolder == null || outputFolder.isEmpty()) { if (outputFolder == null || outputFolder.isEmpty()) {
// If the environment variable is not set, use the default value // If the environment variable is not set, use the default value

View File

@ -0,0 +1,106 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
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.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.PDFText;
import stirling.software.SPDF.pdf.TextFinder;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@Tag(name = "Security", description = "Security APIs")
public class RedactController {
private static final Logger logger = LoggerFactory.getLogger(RedactController.class);
@PostMapping(value = "/auto-redact", consumes = "multipart/form-data")
@Operation(summary = "Redacts listOfText in a PDF document",
description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO")
public ResponseEntity<byte[]> redactPdf(
@Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file,
@Parameter(description = "List of listOfText to redact from the PDF", required = true, schema = @Schema(type = "string")) @RequestParam("listOfText") String listOfTextString,
@RequestParam(value = "useRegex", required = false) boolean useRegex,
@RequestParam(value = "wholeWordSearch", required = false) boolean wholeWordSearchBool,
@RequestParam(value = "customPadding", required = false) float customPadding,
@RequestParam(value = "convertPDFToImage", required = false) boolean convertPDFToImage) throws Exception {
System.out.println(listOfTextString);
String[] listOfText = listOfTextString.split("\n");
byte[] bytes = file.getBytes();
PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes));
for (String text : listOfText) {
text = text.trim();
System.out.println(text);
TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool);
List<PDFText> foundTexts = textFinder.getTextLocations(document);
redactFoundText(document, foundTexts, customPadding);
}
if (convertPDFToImage) {
PDDocument imageDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight()));
imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream = new PDPageContentStream(imageDocument, newPage);
contentStream.drawImage(pdImage, 0, 0);
contentStream.close();
}
document.close();
document = imageDocument;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
document.close();
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(pdfContent,
file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf");
}
private void redactFoundText(PDDocument document, List<PDFText> blocks, float customPadding) throws IOException {
var allPages = document.getDocumentCatalog().getPages();
for (PDFText block : blocks) {
var page = allPages.get(block.getPageIndex());
PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true);
contentStream.setNonStrokingColor(Color.BLACK);
float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding;
PDRectangle pageBox = page.getBBox();
contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding);
contentStream.fill();
contentStream.close();
}
}
}

View File

@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
@ -7,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import stirling.software.SPDF.model.ApplicationProperties;
@Controller @Controller
public class HomeWebController { public class HomeWebController {
@ -31,18 +33,16 @@ public class HomeWebController {
return "redirect:/"; return "redirect:/";
} }
@Autowired
ApplicationProperties applicationProperties;
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody @ResponseBody
@Hidden @Hidden
public String getRobotsTxt() { public String getRobotsTxt() {
String allowGoogleVisibility = System.getProperty("ALLOW_GOOGLE_VISIBILITY"); Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility();
if (allowGoogleVisibility == null) if(Boolean.TRUE.equals(allowGoogle)) {
allowGoogleVisibility = System.getenv("ALLOW_GOOGLE_VISIBILITY");
if (allowGoogleVisibility == null)
allowGoogleVisibility = "false";
if (Boolean.parseBoolean(allowGoogleVisibility)) {
return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /"; return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /";
} else { } else {
return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /"; return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /";

View File

@ -8,6 +8,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -24,26 +25,28 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties;
@RestController @RestController
@RequestMapping("/api/v1") @RequestMapping("/api/v1")
@Tag(name = "API", description = "Info APIs") @Tag(name = "API", description = "Info APIs")
public class MetricsController { public class MetricsController {
@Autowired
ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private boolean isEndpointEnabled; private boolean metricsEnabled;
@PostConstruct @PostConstruct
public void init() { public void init() {
String isEndpointEnabled = System.getProperty("ENABLE_API_METRICS"); Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
if (isEndpointEnabled == null) { if(metricsEnabled == null)
isEndpointEnabled = System.getenv("ENABLE_API_METRICS"); metricsEnabled = true;
if (isEndpointEnabled == null) { this.metricsEnabled = metricsEnabled;
isEndpointEnabled = "true";
}
}
this.isEndpointEnabled = "true".equalsIgnoreCase(isEndpointEnabled);
} }
public MetricsController(MeterRegistry meterRegistry) { public MetricsController(MeterRegistry meterRegistry) {
@ -54,7 +57,7 @@ public class MetricsController {
@Operation(summary = "Application status and version", @Operation(summary = "Application status and version",
description = "This endpoint returns the status of the application and its version number.") description = "This endpoint returns the status of the application and its version number.")
public ResponseEntity<?> getStatus() { public ResponseEntity<?> getStatus() {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
@ -68,7 +71,7 @@ public class MetricsController {
@Operation(summary = "GET request count", @Operation(summary = "GET request count",
description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.")
public ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { public ResponseEntity<?> getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -109,7 +112,7 @@ public class MetricsController {
@Operation(summary = "GET requests count for all endpoints", @Operation(summary = "GET requests count for all endpoints",
description = "This endpoint returns the count of GET requests for each endpoint.") description = "This endpoint returns the count of GET requests for each endpoint.")
public ResponseEntity<?> getAllEndpointLoads() { public ResponseEntity<?> getAllEndpointLoads() {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -170,7 +173,7 @@ public class MetricsController {
@Operation(summary = "POST request count", @Operation(summary = "POST request count",
description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.")
public ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) { public ResponseEntity<?> getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional<String> endpoint) {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -208,7 +211,7 @@ public class MetricsController {
@Operation(summary = "POST requests count for all endpoints", @Operation(summary = "POST requests count for all endpoints",
description = "This endpoint returns the count of POST requests for each endpoint.") description = "This endpoint returns the count of POST requests for each endpoint.")
public ResponseEntity<?> getAllPostRequests() { public ResponseEntity<?> getAllPostRequests() {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
try { try {
@ -244,7 +247,7 @@ public class MetricsController {
@GetMapping("/uptime") @GetMapping("/uptime")
public ResponseEntity<?> getUptime() { public ResponseEntity<?> getUptime() {
if (!isEndpointEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }

View File

@ -11,6 +11,12 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Security", description = "Security APIs") @Tag(name = "Security", description = "Security APIs")
public class SecurityWebController { public class SecurityWebController {
@GetMapping("/auto-redact")
@Hidden
public String autoRedactForm(Model model) {
model.addAttribute("currentPage", "auto-redact");
return "security/auto-redact";
}
@GetMapping("/add-password") @GetMapping("/add-password")
@Hidden @Hidden

View File

@ -0,0 +1,333 @@
package stirling.software.SPDF.model;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import java.util.List;
import java.util.Optional;
import stirling.software.SPDF.config.YamlPropertySourceFactory;
@Configuration
@ConfigurationProperties(prefix = "")
@PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class)
public class ApplicationProperties {
private Security security;
private System system;
private Ui ui;
private Endpoints endpoints;
private Metrics metrics;
private AutomaticallyGenerated automaticallyGenerated;
private AutoPipeline autoPipeline;
public AutoPipeline getAutoPipeline() {
return autoPipeline != null ? autoPipeline : new AutoPipeline();
}
public void setAutoPipeline(AutoPipeline autoPipeline) {
this.autoPipeline = autoPipeline;
}
public Security getSecurity() {
return security != null ? security : new Security();
}
public void setSecurity(Security security) {
this.security = security;
}
public System getSystem() {
return system != null ? system : new System();
}
public void setSystem(System system) {
this.system = system;
}
public Ui getUi() {
return ui != null ? ui : new Ui();
}
public void setUi(Ui ui) {
this.ui = ui;
}
public Endpoints getEndpoints() {
return endpoints != null ? endpoints : new Endpoints();
}
public void setEndpoints(Endpoints endpoints) {
this.endpoints = endpoints;
}
public Metrics getMetrics() {
return metrics != null ? metrics : new Metrics();
}
public void setMetrics(Metrics metrics) {
this.metrics = metrics;
}
public AutomaticallyGenerated getAutomaticallyGenerated() {
return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated();
}
public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) {
this.automaticallyGenerated = automaticallyGenerated;
}
@Override
public String toString() {
return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints="
+ endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated
+ ", autoPipeline=" + autoPipeline + "]";
}
public static class AutoPipeline {
private String outputFolder;
public String getOutputFolder() {
return outputFolder;
}
public void setOutputFolder(String outputFolder) {
this.outputFolder = outputFolder;
}
@Override
public String toString() {
return "AutoPipeline [outputFolder=" + outputFolder + "]";
}
}
public static class Security {
private Boolean enableLogin;
private InitialLogin initialLogin;
private Boolean csrfDisabled;
public Boolean getEnableLogin() {
return enableLogin;
}
public void setEnableLogin(Boolean enableLogin) {
this.enableLogin = enableLogin;
}
public InitialLogin getInitialLogin() {
return initialLogin != null ? initialLogin : new InitialLogin();
}
public void setInitialLogin(InitialLogin initialLogin) {
this.initialLogin = initialLogin;
}
public Boolean getCsrfDisabled() {
return csrfDisabled;
}
public void setCsrfDisabled(Boolean csrfDisabled) {
this.csrfDisabled = csrfDisabled;
}
@Override
public String toString() {
return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled="
+ csrfDisabled + "]";
}
public static class InitialLogin {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]";
}
}
}
public static class System {
private String defaultLocale;
private Boolean googlevisibility;
private String rootPath;
private String customstaticFilePath;
private Integer maxFileSize;
public String getDefaultLocale() {
return defaultLocale;
}
public void setDefaultLocale(String defaultLocale) {
this.defaultLocale = defaultLocale;
}
public Boolean getGooglevisibility() {
return googlevisibility;
}
public void setGooglevisibility(Boolean googlevisibility) {
this.googlevisibility = googlevisibility;
}
public String getRootPath() {
return rootPath;
}
public void setRootPath(String rootPath) {
this.rootPath = rootPath;
}
public String getCustomstaticFilePath() {
return customstaticFilePath;
}
public void setCustomstaticFilePath(String customstaticFilePath) {
this.customstaticFilePath = customstaticFilePath;
}
public Integer getMaxFileSize() {
return maxFileSize;
}
public void setMaxFileSize(Integer maxFileSize) {
this.maxFileSize = maxFileSize;
}
@Override
public String toString() {
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + ", rootPath="
+ rootPath + ", customstaticFilePath=" + customstaticFilePath + ", maxFileSize=" + maxFileSize
+ "]";
}
}
public static class Ui {
private String homeName;
private String homeDescription;
private String navbarName;
public String getHomeName() {
return homeName;
}
public void setHomeName(String homeName) {
this.homeName = homeName;
}
public String getHomeDescription() {
return homeDescription;
}
public void setHomeDescription(String homeDescription) {
this.homeDescription = homeDescription;
}
public String getNavbarName() {
return navbarName;
}
public void setNavbarName(String navbarName) {
this.navbarName = navbarName;
}
@Override
public String toString() {
return "Ui [homeName=" + homeName + ", homeDescription=" + homeDescription + ", navbarName=" + navbarName
+ "]";
}
}
public static class Endpoints {
private List<String> toRemove;
private List<String> groupsToRemove;
public List<String> getToRemove() {
return toRemove;
}
public void setToRemove(List<String> toRemove) {
this.toRemove = toRemove;
}
public List<String> getGroupsToRemove() {
return groupsToRemove;
}
public void setGroupsToRemove(List<String> groupsToRemove) {
this.groupsToRemove = groupsToRemove;
}
@Override
public String toString() {
return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]";
}
}
public static class Metrics {
private Boolean enabled;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
@Override
public String toString() {
return "Metrics [enabled=" + enabled + "]";
}
}
public static class AutomaticallyGenerated {
private String key;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
@Override
public String toString() {
return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]";
}
}
}

View File

@ -0,0 +1,42 @@
package stirling.software.SPDF.model;
public class PDFText {
private final int pageIndex;
private final float x1;
private final float y1;
private final float x2;
private final float y2;
private final String text;
public PDFText(int pageIndex, float x1, float y1, float x2, float y2, String text) {
this.pageIndex = pageIndex;
this.x1 = x1;
this.y1 = y1;
this.x2 = x2;
this.y2 = y2;
this.text = text;
}
public int getPageIndex() {
return pageIndex;
}
public float getX1() {
return x1;
}
public float getY1() {
return y1;
}
public float getX2() {
return x2;
}
public float getY2() {
return y2;
}
public String getText() {
return text;
}
}

View File

@ -0,0 +1,60 @@
package stirling.software.SPDF.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Date;
@Entity
@Table(name = "persistent_logins")
public class PersistentLogin {
@Id
@Column(name = "series")
private String series;
@Column(name = "username", length = 64, nullable = false)
private String username;
@Column(name = "token", length = 64, nullable = false)
private String token;
@Column(name = "last_used", nullable = false)
private Date lastUsed;
public String getSeries() {
return series;
}
public void setSeries(String series) {
this.series = series;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public Date getLastUsed() {
return lastUsed;
}
public void setLastUsed(Date lastUsed) {
this.lastUsed = lastUsed;
}
// Getters, setters, etc.
}

View File

@ -0,0 +1,91 @@
package stirling.software.SPDF.pdf;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.http.ResponseEntity;
import stirling.software.SPDF.model.PDFText;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TextFinder extends PDFTextStripper {
private final String searchText;
private final boolean useRegex;
private final boolean wholeWordSearch;
private final List<PDFText> textOccurrences = new ArrayList<>();
public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) throws IOException {
this.searchText = searchText.toLowerCase();
this.useRegex = useRegex;
this.wholeWordSearch = wholeWordSearch;
setSortByPosition(true);
}
private List<Integer> findOccurrencesInText(String searchText, String content) {
List<Integer> indexes = new ArrayList<>();
Pattern pattern;
if (useRegex) {
// Use regex-based search
pattern = wholeWordSearch
? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)")
: Pattern.compile(searchText);
} else {
// Use normal text search
pattern = wholeWordSearch
? Pattern.compile("(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)")
: Pattern.compile(Pattern.quote(searchText));
}
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
indexes.add(matcher.start());
}
return indexes;
}
@Override
protected void writeString(String text, List<TextPosition> textPositions) {
for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) {
if (index + searchText.length() <= textPositions.size()) {
// Initial values based on the first character
TextPosition first = textPositions.get(index);
float minX = first.getX();
float minY = first.getY();
float maxX = first.getX() + first.getWidth();
float maxY = first.getY() + first.getHeight();
// Loop over the rest of the characters and adjust bounding box values
for (int i = index; i < index + searchText.length(); i++) {
TextPosition position = textPositions.get(i);
minX = Math.min(minX, position.getX());
minY = Math.min(minY, position.getY());
maxX = Math.max(maxX, position.getX() + position.getWidth());
maxY = Math.max(maxY, position.getY() + position.getHeight());
}
textOccurrences.add(new PDFText(
getCurrentPageNo() - 1,
minX,
minY,
maxX,
maxY,
text
));
}
}
}
public List<PDFText> getTextLocations(PDDocument document) throws Exception {
this.getText(document);
System.out.println("Found " + textOccurrences.size() + " occurrences of '" + searchText + "' in the document.");
return textOccurrences;
}
}

View File

@ -0,0 +1,53 @@
package stirling.software.SPDF.repository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import stirling.software.SPDF.model.PersistentLogin;
import stirling.software.SPDF.repository.PersistentLoginRepository;
import java.util.Date;
public class JPATokenRepositoryImpl implements PersistentTokenRepository {
@Autowired
private PersistentLoginRepository persistentLoginRepository;
@Override
public void createNewToken(PersistentRememberMeToken token) {
PersistentLogin newToken = new PersistentLogin();
newToken.setSeries(token.getSeries());
newToken.setUsername(token.getUsername());
newToken.setToken(token.getTokenValue());
newToken.setLastUsed(token.getDate());
persistentLoginRepository.save(newToken);
}
@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentLogin existingToken = persistentLoginRepository.findById(series).orElse(null);
if (existingToken != null) {
existingToken.setToken(tokenValue);
existingToken.setLastUsed(lastUsed);
persistentLoginRepository.save(existingToken);
}
}
@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
PersistentLogin token = persistentLoginRepository.findById(seriesId).orElse(null);
if (token != null) {
return new PersistentRememberMeToken(token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed());
}
return null;
}
@Override
public void removeUserTokens(String username) {
for (PersistentLogin token : persistentLoginRepository.findAll()) {
if (token.getUsername().equals(username)) {
persistentLoginRepository.delete(token);
}
}
}
}

View File

@ -0,0 +1,7 @@
package stirling.software.SPDF.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import stirling.software.SPDF.model.PersistentLogin;
public interface PersistentLoginRepository extends JpaRepository<PersistentLogin, String> {
}

View File

@ -0,0 +1,49 @@
package stirling.software.SPDF.utils;
import java.util.List;
public class PropertyConfigs {
public static boolean getBooleanValue(List<String> keys, boolean defaultValue) {
for (String key : keys) {
String value = System.getProperty(key);
if (value == null)
value = System.getenv(key);
if (value != null)
return Boolean.valueOf(value);
}
return defaultValue;
}
public static String getStringValue(List<String> keys, String defaultValue) {
for (String key : keys) {
String value = System.getProperty(key);
if (value == null)
value = System.getenv(key);
if (value != null)
return value;
}
return defaultValue;
}
public static boolean getBooleanValue(String key, boolean defaultValue) {
String value = System.getProperty(key);
if (value == null)
value = System.getenv(key);
return (value != null) ? Boolean.valueOf(value) : defaultValue;
}
public static String getStringValue(String key, String defaultValue) {
String value = System.getProperty(key);
if (value == null)
value = System.getenv(key);
return (value != null) ? value : defaultValue;
}
}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,27 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=Redact,Hide,black out,black,marker,hidden
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Posición
addPageNumbers.selectText.4=Número de inicio addPageNumbers.selectText.4=Número de inicio
addPageNumbers.selectText.5=Páginas a numerar addPageNumbers.selectText.5=Páginas a numerar
addPageNumbers.selectText.6=Texto personalizado addPageNumbers.selectText.6=Texto personalizado
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Afficher le JavaScript
home.showJS.desc=Recherche et affiche tout JavaScript injecté dans un PDF. home.showJS.desc=Recherche et affiche tout JavaScript injecté dans un PDF.
showJS.tags=afficher,javascript,js showJS.tags=afficher,javascript,js
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=afficher,javascript,js
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Afficher le JavaScript showJS.title=Afficher le JavaScript
showJS.header=Afficher le JavaScript showJS.header=Afficher le JavaScript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Numéro de départ addPageNumbers.selectText.4=Numéro de départ
addPageNumbers.selectText.5=Pages à numéroter addPageNumbers.selectText.5=Pages à numéroter
addPageNumbers.selectText.6=Texte personnalisé addPageNumbers.selectText.6=Texte personnalisé
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Toon Javascript
home.showJS.desc=Zoekt en toont ieder script dat in een PDF is geïnjecteerd home.showJS.desc=Zoekt en toont ieder script dat in een PDF is geïnjecteerd
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Toon Javascript showJS.title=Toon Javascript
showJS.header=Toon Javascript showJS.header=Toon Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Positie
addPageNumbers.selectText.4=Startnummer addPageNumbers.selectText.4=Startnummer
addPageNumbers.selectText.5=Pagina''s om te nummeren addPageNumbers.selectText.5=Pagina''s om te nummeren
addPageNumbers.selectText.6=Aangepaste tekst addPageNumbers.selectText.6=Aangepaste tekst
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Mostrar Javascript
home.showJS.desc=Procura e exibe qualquer JavaScript injetado em um PDF home.showJS.desc=Procura e exibe qualquer JavaScript injetado em um PDF
showJS.tags=JavaScript showJS.tags=JavaScript
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JavaScript
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Exibir JavaScript showJS.title=Exibir JavaScript
showJS.header=Exibir JavaScript showJS.header=Exibir JavaScript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Posição
addPageNumbers.selectText.4=Número Inicial addPageNumbers.selectText.4=Número Inicial
addPageNumbers.selectText.5=Páginas a Numerar addPageNumbers.selectText.5=Páginas a Numerar
addPageNumbers.selectText.6=Texto Personalizado addPageNumbers.selectText.6=Texto Personalizado
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -299,11 +299,33 @@ home.showJS.title=Show Javascript
home.showJS.desc=Searches and displays any JS injected into a PDF home.showJS.desc=Searches and displays any JS injected into a PDF
showJS.tags=JS showJS.tags=JS
##########################
### TODO: Translate ###
##########################
home.autoRedact.title=Auto Redact
home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text
showJS.tags=JS
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
# # # #
########################### ###########################
#auto-redact
##########################
### TODO: Translate ###
##########################
autoRedact.title=Auto Redact
autoRedact.header=Auto Redact
autoRedact.textsToRedactLabel=Text to Redact (line-separated)
autoRedact.textsToRedactPlaceholder=e.g. \nConfidential \nTop-Secret
autoRedact.useRegexLabel=Use Regex
autoRedact.wholeWordSearchLabel=Whole Word Search
autoRedact.customPaddingLabel=Custom Extra Padding
autoRedact.convertPDFToImageLabel=Convert PDF to PDF-Image (Used to remove text behind the box)
autoRedact.submitButton=Submit
#showJS #showJS
showJS.title=Show Javascript showJS.title=Show Javascript
showJS.header=Show Javascript showJS.header=Show Javascript
@ -374,9 +396,6 @@ addPageNumbers.selectText.3=Position
addPageNumbers.selectText.4=Starting Number addPageNumbers.selectText.4=Starting Number
addPageNumbers.selectText.5=Pages to Number addPageNumbers.selectText.5=Pages to Number
addPageNumbers.selectText.6=Custom Text addPageNumbers.selectText.6=Custom Text
##########################
### TODO: Translate ###
##########################
addPageNumbers.customTextDesc=Custom Text addPageNumbers.customTextDesc=Custom Text
addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc addPageNumbers.numberPagesDesc=Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc
addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}

View File

@ -0,0 +1,26 @@
security:
enableLogin: false # set to 'true' to enable login
initialLogin:
username: 'username' # Specify the initial username for first boot (e.g. 'admin')
password: 'password'# Specify the initial password for first boot (e.g. 'password123')
csrfDisabled: true
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility, 'false' to disallow
rootPath: / # Set the application's root URI (e.g. /pdf-app)
customstaticFilePath: '/customFiles/static/' # Directory path for custom static files
maxFileSize: 2000 # Set the maximum file size in MB
ui:
homeName: # Application's visible name
homeDescription: # Short description or tagline.
navbarName: # Name displayed on the navigation bar
endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable metric API endpoints, 'false' to disable

File diff suppressed because one or more lines are too long

View File

@ -14,7 +14,7 @@
.features-container { .features-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(21rem, 3fr)); grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr));
gap: 25px 30px; gap: 25px 30px;
} }
@ -27,6 +27,8 @@
align-items: flex-start; align-items: flex-start;
background: rgba(13, 110, 253, 0.05); background: rgba(13, 110, 253, 0.05);
transition: transform 0.3s, border 0.3s; transition: transform 0.3s, border 0.3s;
transform-origin: center center;
outline: 2px solid transparent;
} }
.feature-card a { .feature-card a {
@ -43,7 +45,7 @@
} }
.feature-card:hover { .feature-card:hover {
border: 1px solid rgba(0, 0, 0, .5); outline: 1px solid rgba(0, 0, 0, .5);
cursor: pointer; cursor: pointer;
transform: scale(1.1); transform: scale(1.1);
} }

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eraser-fill" viewBox="0 0 16 16">
<path d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828l6.879-6.879zm.66 11.34L3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293l.16-.16z"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

View File

@ -13,12 +13,12 @@ const DraggableUtils = {
listeners: { listeners: {
move: (event) => { move: (event) => {
const target = event.target; const target = event.target;
const x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx; const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy; const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`; target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute('data-x', x); target.setAttribute('data-bs-x', x);
target.setAttribute('data-y', y); target.setAttribute('data-bs-y', y);
this.onInteraction(target); this.onInteraction(target);
}, },
@ -29,8 +29,8 @@ const DraggableUtils = {
listeners: { listeners: {
move: (event) => { move: (event) => {
var target = event.target var target = event.target
var x = (parseFloat(target.getAttribute('data-x')) || 0) var x = (parseFloat(target.getAttribute('data-bs-x')) || 0)
var y = (parseFloat(target.getAttribute('data-y')) || 0) var y = (parseFloat(target.getAttribute('data-bs-y')) || 0)
// check if control key is pressed // check if control key is pressed
if (event.ctrlKey) { if (event.ctrlKey) {
@ -58,8 +58,8 @@ const DraggableUtils = {
target.style.transform = 'translate(' + x + 'px,' + y + 'px)' target.style.transform = 'translate(' + x + 'px,' + y + 'px)'
target.setAttribute('data-x', x) target.setAttribute('data-bs-x', x)
target.setAttribute('data-y', y) target.setAttribute('data-bs-y', y)
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height) target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height)
this.onInteraction(target); this.onInteraction(target);
@ -86,8 +86,8 @@ const DraggableUtils = {
const x = 0; const x = 0;
const y = 20; const y = 20;
createdCanvas.style.transform = `translate(${x}px, ${y}px)`; createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
createdCanvas.setAttribute('data-x', x); createdCanvas.setAttribute('data-bs-x', x);
createdCanvas.setAttribute('data-y', y); createdCanvas.setAttribute('data-bs-y', y);
createdCanvas.onclick = e => this.onInteraction(e.target); createdCanvas.onclick = e => this.onInteraction(e.target);

View File

@ -1,37 +1,45 @@
function updateFavoritesDropdown() { function updateFavoritesDropdown() {
var dropdown = document.querySelector('#favoritesDropdown'); var dropdown = document.querySelector('#favoritesDropdown');
dropdown.innerHTML = ''; // Clear the current favorites
// Check if dropdown exists
if (!dropdown) {
console.error('Dropdown element with ID "favoritesDropdown" not found!');
return; // Exit the function
}
dropdown.innerHTML = ''; // Clear the current favorites
var hasFavorites = false;
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
if (localStorage.getItem(key) === 'favorite') {
// Find the corresponding navbar entry
var navbarEntry = document.querySelector(`a[href='${key}']`);
if (navbarEntry) {
// Create a new dropdown entry
var dropdownItem = document.createElement('a');
dropdownItem.className = 'dropdown-item';
dropdownItem.href = navbarEntry.href;
dropdownItem.innerHTML = navbarEntry.innerHTML;
dropdown.appendChild(dropdownItem);
hasFavorites = true;
} else {
console.warn(`Navbar entry not found for key: ${key}`);
}
}
}
var hasFavorites = false; // Show or hide the default item based on whether there are any favorites
if (!hasFavorites) {
for (var i = 0; i < localStorage.length; i++) { var defaultItem = document.createElement('a');
var key = localStorage.key(i); defaultItem.className = 'dropdown-item';
if (localStorage.getItem(key) === 'favorite') { defaultItem.textContent = noFavourites;
// Find the corresponding navbar entry dropdown.appendChild(defaultItem);
var navbarEntry = document.querySelector(`a[href='${key}']`); }
if (navbarEntry) {
// Create a new dropdown entry
var dropdownItem = document.createElement('a');
dropdownItem.className = 'dropdown-item';
dropdownItem.href = navbarEntry.href;
dropdownItem.innerHTML = navbarEntry.innerHTML;
dropdown.appendChild(dropdownItem);
hasFavorites = true;
}
}
}
// Show or hide the default item based on whether there are any favorites
if (!hasFavorites) {
var defaultItem = document.createElement('a');
defaultItem.className = 'dropdown-item';
defaultItem.textContent = noFavourites;
dropdown.appendChild(defaultItem);
}
} }
document.addEventListener('DOMContentLoaded', function() {
updateFavoritesDropdown(); // Ensure that the DOM content has been fully loaded before calling the function
}); document.addEventListener('DOMContentLoaded', function() {
console.log('DOMContentLoaded event fired');
updateFavoritesDropdown();
});

View File

@ -3,9 +3,9 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
function setupFileInput(chooser) { function setupFileInput(chooser) {
const elementId = chooser.getAttribute('data-element-id'); const elementId = chooser.getAttribute('data-bs-element-id');
const filesSelected = chooser.getAttribute('data-files-selected'); const filesSelected = chooser.getAttribute('data-bs-files-selected');
const pdfPrompt = chooser.getAttribute('data-pdf-prompt'); const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt');
let allFiles = []; let allFiles = [];
let overlay; let overlay;

View File

@ -10,7 +10,7 @@ function filterCards() {
// Get the navbar tags associated with the card // Get the navbar tags associated with the card
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`); var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
var navbarTags = navbarItem ? navbarItem.getAttribute('data-tags') : ''; var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : '';
var content = title + ' ' + text + ' ' + navbarTags; var content = title + ' ' + text + ' ' + navbarTags;

View File

@ -14,33 +14,40 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
function handleDropdownItemClick(event) { function handleDropdownItemClick(event) {
event.preventDefault(); event.preventDefault();
const languageCode = this.dataset.languageCode; const languageCode = event.currentTarget.dataset.bsLanguageCode; // change this to event.currentTarget
localStorage.setItem('languageCode', languageCode); if (languageCode) {
localStorage.setItem('languageCode', languageCode);
const currentUrl = window.location.href; const currentUrl = window.location.href;
if (currentUrl.indexOf('?lang=') === -1) { if (currentUrl.indexOf('?lang=') === -1) {
window.location.href = currentUrl + '?lang=' + languageCode; window.location.href = currentUrl + '?lang=' + languageCode;
} else { } else {
window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode);
} }
} else {
console.error("Language code is not set for this item."); // for debugging
}
} }
$(document).ready(function() {
$(".nav-item.dropdown").each(function() {
var $dropdownMenu = $(this).find(".dropdown-menu");
if ($dropdownMenu.children().length <= 2 && $dropdownMenu.children("hr.dropdown-divider").length === $dropdownMenu.children().length) {
$(this).prev('.nav-item.nav-item-separator').remove();
$(this).remove();
}
});
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.nav-item.dropdown').forEach((element) => {
const dropdownMenu = element.querySelector(".dropdown-menu");
if (dropdownMenu.id !== 'favoritesDropdown' && dropdownMenu.children.length <= 2 && dropdownMenu.querySelectorAll("hr.dropdown-divider").length === dropdownMenu.children.length) {
if (element.previousElementSibling && element.previousElementSibling.classList.contains('nav-item') && element.previousElementSibling.classList.contains('nav-item-separator')) {
element.previousElementSibling.remove();
}
element.remove();
}
});
//Sort languages by alphabet //Sort languages by alphabet
var list = $('.dropdown-menu[aria-labelledby="languageDropdown"]').children("a"); const list = Array.from(document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').children).filter(child => child.matches('a'));
list.sort(function(a, b) { list.sort(function(a, b) {
var A = $(a).text().toUpperCase(); var A = a.textContent.toUpperCase();
var B = $(b).text().toUpperCase(); var B = b.textContent.toUpperCase();
return (A < B) ? -1 : (A > B) ? 1 : 0; return (A < B) ? -1 : (A > B) ? 1 : 0;
}) }).forEach(node => document.querySelector('.dropdown-menu[aria-labelledby="languageDropdown"]').appendChild(node));
.appendTo('.dropdown-menu[aria-labelledby="languageDropdown"]');
}); });

View File

@ -241,7 +241,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
if (parameter.name === 'fileInput') return; if (parameter.name === 'fileInput') return;
let parameterDiv = document.createElement('div'); let parameterDiv = document.createElement('div');
parameterDiv.className = "form-group"; parameterDiv.className = "mb-3";
let parameterLabel = document.createElement('label'); let parameterLabel = document.createElement('label');
parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `;

View File

@ -43,7 +43,7 @@ document.querySelector('#navbarSearchInput').addEventListener('input', function(
var titleElement = item.querySelector('.icon-text'); var titleElement = item.querySelector('.icon-text');
var iconElement = item.querySelector('.icon'); var iconElement = item.querySelector('.icon');
var itemHref = item.getAttribute('href'); var itemHref = item.getAttribute('href');
var tags = item.getAttribute('data-tags') || ""; // If no tags, default to empty string var tags = item.getAttribute('data-bs-tags') || ""; // If no tags, default to empty string
if (titleElement && iconElement && itemHref !== '#') { if (titleElement && iconElement && itemHref !== '#') {
var title = titleElement.innerText; var title = titleElement.innerText;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -3,12 +3,12 @@
"short_name": "Stirling PDF", "short_name": "Stirling PDF",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/android-chrome-512x512.png", "src": "android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
@ -16,4 +16,4 @@
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{account.title})}"></th:block>
@ -24,15 +24,15 @@
<!-- Change Username Form --> <!-- Change Username Form -->
<h4></h4> <h4></h4>
<form action="/change-username" method="post"> <form action="/change-username" method="post">
<div class="form-group"> <div class="mb-3">
<label for="newUsername" th:text="#{account.changeUsername}">Change Username</label> <label for="newUsername" th:text="#{account.changeUsername}">Change Username</label>
<input type="text" class="form-control" name="newUsername" id="newUsername" placeholder="New Username"> <input type="text" class="form-control" name="newUsername" id="newUsername" placeholder="New Username">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="currentPassword" th:text="#{password}">Password</label> <label for="currentPassword" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordUsername" placeholder="Password"> <input type="password" class="form-control" name="currentPassword" id="currentPasswordUsername" placeholder="Password">
</div> </div>
<div class="form-group"> <div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changeUsername}">Change Username</button> <button type="submit" class="btn btn-primary" th:text="#{account.changeUsername}">Change Username</button>
</div> </div>
</form> </form>
@ -42,19 +42,19 @@
<!-- Change Password Form --> <!-- Change Password Form -->
<h4 th:text="#{account.changePassword}">Change Password?</h4> <h4 th:text="#{account.changePassword}">Change Password?</h4>
<form action="/change-password" method="post"> <form action="/change-password" method="post">
<div class="form-group"> <div class="mb-3">
<label for="currentPassword" th:text="#{account.oldPassword}">Old Password</label> <label for="currentPassword" th:text="#{account.oldPassword}">Old Password</label>
<input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{account.oldPassword}"> <input type="password" class="form-control" name="currentPassword" id="currentPasswordPassword" th:placeholder="#{account.oldPassword}">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="newPassword" th:text="#{account.newPassword}">New Password</label> <label for="newPassword" th:text="#{account.newPassword}">New Password</label>
<input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}"> <input type="password" class="form-control" name="newPassword" id="newPassword" th:placeholder="#{account.newPassword}">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label> <label for="confirmNewPassword" th:text="#{account.confirmNewPassword}">Confirm New Password</label>
<input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}"> <input type="password" class="form-control" name="confirmNewPassword" id="confirmNewPassword" th:placeholder="#{account.confirmNewPassword}">
</div> </div>
<div class="form-group"> <div class="mb-3">
<button type="submit" class="btn btn-primary" th:text="#{account.changePassword}">Change Password</button> <button type="submit" class="btn btn-primary" th:text="#{account.changePassword}">Change Password</button>
</div> </div>
</form> </form>
@ -286,7 +286,7 @@
<div class="form-group mt-4"> <div class="mb-3 mt-4">
<a href="/logout"> <a href="/logout">
<button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button> <button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button>
</a> </a>

View File

@ -41,15 +41,15 @@
<h2 th:text="#{adminUserSettings.addUser}">Add New User</h2> <h2 th:text="#{adminUserSettings.addUser}">Add New User</h2>
<form action="/admin/saveUser" method="post"> <form action="/admin/saveUser" method="post">
<div class="form-group"> <div class="mb-3">
<label for="username" th:text="#{username}">Username</label> <label for="username" th:text="#{username}">Username</label>
<input type="text" class="form-control" name="username" required> <input type="text" class="form-control" name="username" required>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="password" th:text="#{password}">Password</label> <label for="password" th:text="#{password}">Password</label>
<input type="password" class="form-control" name="password" required> <input type="password" class="form-control" name="password" required>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="role" th:text="#{adminUserSettings.role}">Role</label> <label for="role" th:text="#{adminUserSettings.role}">Role</label>
<select name="role" class="form-control" required> <select name="role" class="form-control" required>
<option value="ROLE_ADMIN" th:text="#{adminUserSettings.admin}">Admin</option> <option value="ROLE_ADMIN" th:text="#{adminUserSettings.admin}">Admin</option>

View File

@ -27,7 +27,7 @@
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false)}"></div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="duplexMode" id="duplexMode"> <input type="checkbox" class="form-check-input" name="duplexMode" id="duplexMode">
<label class="ml-3" for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label> <label class="ms-3" for="duplexMode" th:text=#{autoSplitPDF.duplexMode}></label>
</div> </div>
<p><a th:href="@{files/Auto Splitter Divider (minimal).pdf}" download th:text="#{autoSplitPDF.dividerDownload1}"></a></p> <p><a th:href="@{files/Auto Splitter Divider (minimal).pdf}" download th:text="#{autoSplitPDF.dividerDownload1}"></a></p>
<p><a th:href="@{files/Auto Splitter Divider (with instructions).pdf}" download th:text="#{autoSplitPDF.dividerDownload2}"></a></p> <p><a th:href="@{files/Auto Splitter Divider (with instructions).pdf}" download th:text="#{autoSplitPDF.dividerDownload2}"></a></p>

View File

@ -23,13 +23,13 @@
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="stretchToFit" id="stretchToFit"> <input type="checkbox" class="form-check-input" name="stretchToFit" id="stretchToFit">
<label class="ml-3" for="stretchToFit" th:text=#{imageToPDF.selectText.1}></label> <label class="ms-3" for="stretchToFit" th:text=#{imageToPDF.selectText.1}></label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input" name="autoRotate" id="autoRotate"> <input type="checkbox" class="form-check-input" name="autoRotate" id="autoRotate">
<label class="ml-3" for="autoRotate" th:text=#{imageToPDF.selectText.2}></label> <label class="ms-3" for="autoRotate" th:text=#{imageToPDF.selectText.2}></label>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label> <label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" id="colorType" name="colorType"> <select class="form-control" id="colorType" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option> <option value="color" th:text="#{pdfToImage.color}"></option>
@ -39,7 +39,7 @@
</div> </div>
<br> <br>
<input type="hidden" id="override" name="override" value="multi"> <input type="hidden" id="override" name="override" value="multi">
<div class="form-group"> <div class="mb-3">
<label th:text=#{imageToPDF.selectText.3}></label> <label th:text=#{imageToPDF.selectText.3}></label>
<select class="form-control" id="conversionType" name="conversionType" disabled> <select class="form-control" id="conversionType" name="conversionType" disabled>
<option value="merge" th:text=#{imageToPDF.selectText.4}></option> <option value="merge" th:text=#{imageToPDF.selectText.4}></option>

View File

@ -18,7 +18,7 @@
<p th:text="#{processTimeWarning}"></p> <p th:text="#{processTimeWarning}"></p>
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-img}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-img}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{pdfToImage.selectText}"></label> <label th:text="#{pdfToImage.selectText}"></label>
<select class="form-control" name="imageFormat"> <select class="form-control" name="imageFormat">
<option value="png">PNG</option> <option value="png">PNG</option>
@ -26,14 +26,14 @@
<option value="gif">GIF</option> <option value="gif">GIF</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{pdfToImage.singleOrMultiple}"></label> <label th:text="#{pdfToImage.singleOrMultiple}"></label>
<select class="form-control" name="singleOrMultiple"> <select class="form-control" name="singleOrMultiple">
<option value="single" th:text="#{pdfToImage.single}"></option> <option value="single" th:text="#{pdfToImage.single}"></option>
<option value="multiple" th:text="#{pdfToImage.multi}"></option> <option value="multiple" th:text="#{pdfToImage.multi}"></option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{pdfToImage.colorType}"></label> <label th:text="#{pdfToImage.colorType}"></label>
<select class="form-control" name="colorType"> <select class="form-control" name="colorType">
<option value="color" th:text="#{pdfToImage.color}"></option> <option value="color" th:text="#{pdfToImage.color}"></option>
@ -41,7 +41,7 @@
<option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option> <option value="blackwhite" th:text="#{pdfToImage.blackwhite}"></option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="dpi">DPI:</label> <label for="dpi">DPI:</label>
<input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required> <input type="number" name="dpi" class="form-control" id="dpi" min="1" step="1" value="300" required>
</div> </div>

View File

@ -15,7 +15,7 @@
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-presentation}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-presentation}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{PDFToPresentation.selectText.1}"></label> <label th:text="#{PDFToPresentation.selectText.1}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="ppt">PPT</option> <option value="ppt">PPT</option>

View File

@ -15,7 +15,7 @@
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-text}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-text}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{PDFToText.selectText.1}"></label> <label th:text="#{PDFToText.selectText.1}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="rtf">RTF</option> <option value="rtf">RTF</option>

View File

@ -15,7 +15,7 @@
<form method="post" enctype="multipart/form-data" th:action="@{pdf-to-word}"> <form method="post" enctype="multipart/form-data" th:action="@{pdf-to-word}">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{PDFToWord.selectText.1}"></label> <label th:text="#{PDFToWord.selectText.1}"></label>
<select class="form-control" name="outputFormat"> <select class="form-control" name="outputFormat">
<option value="doc">Doc</option> <option value="doc">Doc</option>

View File

@ -16,7 +16,7 @@
<form th:action="@{rearrange-pages}" method="post" enctype="multipart/form-data"> <form th:action="@{rearrange-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<input type="hidden" id="customMode" name="customMode" value=""> <input type="hidden" id="customMode" name="customMode" value="">
<div class="form-group"> <div class="mb-3">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label> <label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" required> <input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,2,8 or 4,7,12-16 or 2n-1)" required>
</div> </div>

View File

@ -1,8 +1,8 @@
<div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" data-tags="${tags}"> <div th:fragment="card" class="feature-card" th:id="${id}" th:if="${@endpointConfiguration.isEndpointEnabled(cardLink)}" data-bs-tags="${tags}">
<a th:href="${cardLink}"> <a th:href="${cardLink}">
<div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title --> <div class="d-flex align-items-center"> <!-- Add a flex container to align the SVG and title -->
<img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30"> <img th:if="${svgPath}" id="card-icon" class="home-card-icon home-card-icon-colour" th:src="${svgPath}" alt="Icon" width="30" height="30">
<h5 class="card-title ml-2" th:text="${cardTitle}"></h5> <!-- Add some margin-left (ml-2) for spacing between SVG and title --> <h5 class="card-title ms-2" th:text="${cardTitle}"></h5> <!-- Add some margin-left (ms-2) for spacing between SVG and title -->
</div> </div>
<p class="card-text" th:text="${cardText}"></p> <p class="card-text" th:text="${cardText}"></p>
</a> </a>

View File

@ -35,7 +35,6 @@
<script src="js/thirdParty/popper.min.js"></script> <script src="js/thirdParty/popper.min.js"></script>
<script src="js/thirdParty/bootstrap.min.js"></script> <script src="js/thirdParty/bootstrap.min.js"></script>
<link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/bootstrap-icons.css">
<!-- PDF.js --> <!-- PDF.js -->
<script src="pdfjs/pdf.js"></script> <script src="pdfjs/pdf.js"></script>
@ -57,7 +56,7 @@
</head> </head>
<th:block th:fragment="game"> <th:block th:fragment="game">
<dialog id="game-container-wrapper" class="game-container-wrapper" data-modal> <dialog id="game-container-wrapper" class="game-container-wrapper" data-bs-modal>
<script> <script>
console.log("loaded game") console.log("loaded game")
$(document).ready(function() { $(document).ready(function() {
@ -116,20 +115,20 @@
</script> </script>
<script src="js/downloader.js"></script> <script src="js/downloader.js"></script>
<div class="custom-file-chooser" th:attr="data-unique-id=${name}, <div class="custom-file-chooser" th:attr="data-bs-unique-id=${name},
data-element-id=${name+'-input'}, data-bs-element-id=${name+'-input'},
data-files-selected=#{filesSelected}, data-bs-files-selected=#{filesSelected},
data-pdf-prompt=#{pdfPrompt}"> data-bs-pdf-prompt=#{pdfPrompt}">
<div class="custom-file"> <div class="mb-3">
<input type="file" class="custom-file-input" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple th:classappend="${notRequired ? '' : 'required'}"> <input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" multiple th:classappend="${notRequired ? '' : 'required'}">
<label class="custom-file-label" th:for="${name}+'-input'" th:text="${inputText}"></label> </div>
</div> <div class="selected-files"></div>
<div class="selected-files"></div> </div>
</div>
<div id="progressBarContainer" style="display: none; position: relative;"> <div id="progressBarContainer" style="display: none; position: relative;">
<div class="progress" style="height: 1rem;"> <div class="progress" style="height: 1rem;">
<div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"> <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
<span class="sr-only">Loading...</span> <span class="visually-hidden">Loading...</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -24,7 +24,7 @@
<p th:text="${message} + ' for path: ' + ${path}"></p> <p th:text="${message} + ' for path: ' + ${path}"></p>
<button type="button" class="btn btn-danger" th:if="${trace}" onclick="toggletrace()">Show Stack Trace</button> <button type="button" class="btn btn-danger" th:if="${trace}" onclick="toggletrace()">Show Stack Trace</button>
<button type="button" class="btn btn-secondary" th:if="${trace}" onclick="copytrace()">Copy Stack Trace</button> <button type="button" class="btn btn-secondary" th:if="${trace}" onclick="copytrace()">Copy Stack Trace</button>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="dismissError()"> <button type="button" class="close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<!-- Stack trace section --> <!-- Stack trace section -->

View File

@ -7,7 +7,7 @@
<button type="button" class="btn btn-danger" onclick="toggletrace()">Show Stack Trace</button> <button type="button" class="btn btn-danger" onclick="toggletrace()">Show Stack Trace</button>
<button type="button" class="btn btn-secondary" onclick="copytrace()">Copy Stack Trace</button> <button type="button" class="btn btn-secondary" onclick="copytrace()">Copy Stack Trace</button>
<button type="button" class="btn btn-info" onclick="showHelp()">Help</button> <button type="button" class="btn btn-info" onclick="showHelp()">Help</button>
<button type="button" class="close" data-dismiss="alert" aria-label="Close" onclick="dismissError()"> <button type="button" class="close" data-bs-dismiss="alert" aria-label="Close" onclick="dismissError()">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
<!-- Stack trace section --> <!-- Stack trace section -->
@ -29,7 +29,7 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="helpModalLabel">Help</h5> <h5 class="modal-title" id="helpModalLabel">Help</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
@ -47,7 +47,7 @@
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Discord - Submit Support post</a> <a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" target="_blank">Discord - Submit Support post</a>
</div> </div>
<a href="/" id="home-button">Go to Homepage</a> <a href="/" id="home-button">Go to Homepage</a>
<a data-dismiss="modal" id="home-button">Close</a> <a data-bs-dismiss="modal" id="home-button">Close</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,75 @@
<th:block th:fragment="langAndDarkMode">
<script src="js/languageSelection.js"></script>
<script src="js/darkmode.js"></script>
<li class="nav-item">
<a class="nav-link" id="dark-mode-toggle" href="#">
<img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" />
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2" viewBox="0 0 20 20">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
</svg>
</a>
<div class="dropdown-menu" aria-labelledby="languageDropdown">
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ar_AR">
<img src="images/flags/sa.svg" alt="icon" width="20" height="15"> العربية
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ca_CA">
<img src="images/flags/es-ct.svg" alt="icon" width="20" height="15"> Català
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="zh_CN">
<img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="de_DE">
<img src="images/flags/de.svg" alt="icon" width="20" height="15"> Deutsch
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="en_GB">
<img src="images/flags/gb.svg" alt="icon" width="20" height="15"> English (GB)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="en_US">
<img src="images/flags/us.svg" alt="icon" width="20" height="15"> English (US)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="eu_ES">
<img src="images/flags/eu.svg" alt="icon" width="20" height="15"> Euskara
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="es_ES">
<img src="images/flags/es.svg" alt="icon" width="20" height="15"> Español
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="fr_FR">
<img src="images/flags/fr.svg" alt="icon" width="20" height="15"> Français
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="it_IT">
<img src="images/flags/it.svg" alt="icon" width="20" height="15"> Italiano
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="nl_NL">
<img src="images/flags/nl.svg" alt="icon" width="20" height="15"> Nederlands
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pl_PL">
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="pt_BR">
<img src="images/flags/pt_br.svg" alt="icon" width="20" height="15"> Português (BR)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ro_RO">
<img src="images/flags/ro.svg" alt="icon" width="20" height="15"> Romanian
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="sv_SE">
<img src="images/flags/se.svg" alt="icon" width="20" height="15"> Svenska
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ru_RU">
<img src="images/flags/ru.svg" alt="icon" width="20" height="15"> Русский
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ko_KR">
<img src="images/flags/kr.svg" alt="icon" width="20" height="15"> 한국어
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-bs-language-code="ja_JP">
<img src="images/flags/jp.svg" alt="icon" width="20" height="15"> 日本語
</a>
</div>
</li>
</th:block>

View File

@ -18,13 +18,13 @@
</a> </a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto flex-nowrap"> <ul class="navbar-nav me-auto flex-nowrap">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}"> <a class="nav-link" href="#" th:href="@{multi-tool}" th:classappend="${currentPage}=='multi-tool' ? 'active' : ''" th:title="#{home.multiTool.desc}">
@ -41,7 +41,7 @@
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/file-earmark-pdf.svg" alt="icon"> <img class="icon" src="images/file-earmark-pdf.svg" alt="icon">
<span class="icon-text" th:text="#{navbar.pageOps}"></span> <span class="icon-text" th:text="#{navbar.pageOps}"></span>
</a> </a>
@ -64,7 +64,7 @@
</div> </div>
</li> </li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='pdf-to-pdfa' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' OR ${currentPage}=='pdf-to-word' OR ${currentPage}=='pdf-to-presentation' OR ${currentPage}=='pdf-to-text' OR ${currentPage}=='pdf-to-html' OR ${currentPage}=='pdf-to-xml' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" <li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='pdf-to-pdfa' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' OR ${currentPage}=='pdf-to-word' OR ${currentPage}=='pdf-to-presentation' OR ${currentPage}=='pdf-to-text' OR ${currentPage}=='pdf-to-html' OR ${currentPage}=='pdf-to-xml' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"> aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.convert}"></span> <span class="icon-text" th:text="#{navbar.convert}"></span>
@ -94,7 +94,7 @@
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span> <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text" th:text="#{navbar.security}"></span>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
@ -104,13 +104,13 @@
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div> <div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div>
<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'images/eraser-fill.svg', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags')}"></div>
</div> </div>
</li> </li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='show-javascript' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='get-info-on-pdf' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='show-javascript' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='get-info-on-pdf' ? 'active' : ''">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
<span class="icon-text" th:text="#{navbar.other}"></span> <span class="icon-text" th:text="#{navbar.other}"></span>
@ -137,9 +137,9 @@
</ul> </ul>
<ul class="navbar-nav ml-auto flex-nowrap"> <ul class="navbar-nav flex-nowrap">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<img class="navbar-icon" src="images/star.svg" alt="icon" width="24" height="24"> <img class="navbar-icon" src="images/star.svg" alt="icon" width="24" height="24">
</a> </a>
<div class="dropdown-menu" id="favoritesDropdown" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" id="favoritesDropdown" aria-labelledby="navbarDropdown">
@ -147,80 +147,12 @@
</div> </div>
</li> </li>
<li class="nav-item"> <th:block th:insert="~{fragments/langAndDarkMode :: langAndDarkMode}"></th:block>
<a class="nav-link" id="dark-mode-toggle" href="#">
<img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" />
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2" viewBox="0 0 20 20">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/>
</svg>
</a>
<div class="dropdown-menu" aria-labelledby="languageDropdown">
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ar_AR">
<img src="images/flags/sa.svg" alt="icon" width="20" height="15"> العربية
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ca_CA">
<img src="images/flags/es-ct.svg" alt="icon" width="20" height="15"> Català
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="zh_CN">
<img src="images/flags/cn.svg" alt="icon" width="20" height="15"> 简体中文
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="de_DE">
<img src="images/flags/de.svg" alt="icon" width="20" height="15"> Deutsch
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_GB">
<img src="images/flags/gb.svg" alt="icon" width="20" height="15"> English (GB)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="en_US">
<img src="images/flags/us.svg" alt="icon" width="20" height="15"> English (US)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="eu_ES">
<img src="images/flags/eu.svg" alt="icon" width="20" height="15"> Euskara
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="es_ES">
<img src="images/flags/es.svg" alt="icon" width="20" height="15"> Español
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="fr_FR">
<img src="images/flags/fr.svg" alt="icon" width="20" height="15"> Français
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="it_IT">
<img src="images/flags/it.svg" alt="icon" width="20" height="15"> Italiano
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="nl_NL">
<img src="images/flags/nl.svg" alt="icon" width="20" height="15"> Nederlands
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pt_BR">
<img src="images/flags/pt_br.svg" alt="icon" width="20" height="15"> Português (BR)
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ro_RO">
<img src="images/flags/ro.svg" alt="icon" width="20" height="15"> Romanian
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="sv_SE">
<img src="images/flags/se.svg" alt="icon" width="20" height="15"> Svenska
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ru_RU">
<img src="images/flags/ru.svg" alt="icon" width="20" height="15"> Русский
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ko_KR">
<img src="images/flags/kr.svg" alt="icon" width="20" height="15"> 한국어
</a>
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="ja_JP">
<img src="images/flags/jp.svg" alt="icon" width="20" height="15"> 日本語
</a>
</div>
</li>
<li class="nav-item"> <li class="nav-item">
<!-- Settings Button --> <!-- Settings Button -->
<a href="#" class="nav-link" data-toggle="modal" data-target="#settingsModal"> <a href="#" class="nav-link" data-bs-toggle="modal" data-bs-target="#settingsModal">
<img class="navbar-icon" src="images/gear.svg" alt="icon" width="24" height="24"> <img class="navbar-icon" src="images/gear.svg" alt="icon" width="24" height="24">
</a> </a>
@ -305,9 +237,7 @@
<div class="modal-content dark-card"> <div class="modal-content dark-card">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5> <h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<span aria-hidden="true">&times;</span>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
@ -318,14 +248,11 @@
<a href="swagger-ui/index.html" target="_blank"> <a href="swagger-ui/index.html" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary">API</button> <button type="button" class="btn btn-sm btn-outline-primary">API</button>
</a> </a>
<a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank"> <a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button> <button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button>
</a> </a>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label> <label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label>
<select class="form-control" id="downloadOption"> <select class="form-control" id="downloadOption">
<option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option> <option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option>
@ -333,37 +260,31 @@
<option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option> <option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label> <label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label>
<input type="range" class="custom-range" min="1" max="9" step="1" id="zipThreshold" value="4"> <input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4">
<span id="zipThresholdValue" class="ml-2"></span> <span id="zipThresholdValue" class="ms-2"></span>
</div> </div>
<div class="form-group"> <div class="mb-3 form-check">
<div class="custom-control custom-checkbox"> <input type="checkbox" class="form-check-input" id="boredWaiting">
<input type="checkbox" class="custom-control-input" id="boredWaiting"> <label class="form-check-label" for="boredWaiting" th:text="#{bored}"></label>
<label class="custom-control-label" for="boredWaiting" th:text="#{bored}"></label>
</div>
</div> </div>
<a th:if="${@loginEnabled}" href="account" target="_blank"> <a th:if="${@loginEnabled}" href="account" target="_blank">
<button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button> <button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button>
</a> </a>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a th:if="${@loginEnabled}" href="/logout"> <a th:if="${@loginEnabled}" href="/logout">
<button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button> <button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button>
</a> </a>
<button type="button" class="btn btn-secondary" data-dismiss="modal" th:text="#{close}"></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script src="js/settings.js"></script> <script src="js/settings.js"></script>

View File

@ -1,5 +1,5 @@
<div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey, tagKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}"> <div th:fragment="navbarEntry (endpoint, imgSrc, titleKey, descKey, tagKey)" th:if="${@endpointConfiguration.isEndpointEnabled(endpoint)}">
<a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}" th:data-tags="#{${tagKey}}"> <a class="dropdown-item" href="#" th:href="@{${endpoint}}" th:classappend="${endpoint.equals(currentPage)} ? 'active' : ''" th:title="#{${descKey}}" th:data-bs-tags="#{${tagKey}}">
<img class="icon" th:src="@{${imgSrc}}" alt="icon"> <img class="icon" th:src="@{${imgSrc}}" alt="icon">
<span class="icon-text" th:text="#{${titleKey}}"></span> <span class="icon-text" th:text="#{${titleKey}}"></span>
</a> </a>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
@ -23,6 +23,7 @@
<script src="js/homecard.js"></script> <script src="js/homecard.js"></script>
<div class=" container"> <div class=" container">
<br>
<input type="text" id="searchBar" onkeyup="filterCards()" placeholder="Search for features..."> <input type="text" id="searchBar" onkeyup="filterCards()" placeholder="Search for features...">
<div class="features-container "> <div class="features-container ">
@ -89,6 +90,7 @@
<div th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', svgPath='images/extract.svg')}"></div> <div th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', svgPath='images/extract.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', svgPath='images/single-page.svg')}"></div> <div th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', svgPath='images/single-page.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', svgPath='images/js.svg')}"></div> <div th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', svgPath='images/js.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', svgPath='images/eraser-fill.svg')}"></div>

View File

@ -1,59 +1,99 @@
<!-- Hi if you have been redirected here when using API then you might need to supply a X-API-KEY key in header to authenticate! -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title> <th:block th:insert="~{fragments/common :: head(title=#{login.title})}"></th:block>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"> <style>
</head> html, body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.container-flex {
display: flex;
flex-direction: column;
min-height: 100vh;
width: 100%; /* Set width to 100% */
align-items: center; /* Center its children horizontally */
}
.footer-bottom {
margin-top: auto;
}
</style>
<body> <body>
<div class="container-flex">
<main class="form-signin text-center">
<div th:if="${logoutMessage}" class="alert alert-success"
th:text="${logoutMessage}"></div>
<div id="page-container"> <form th:action="@{/login}" method="post">
<div id="content-wrap"> <img class="mb-4" src="favicon.svg" alt="" width="144" height="144">
<div class="container"> <h1 class="h1 mb-3 fw-normal">Stirling-PDF</h1>
<div class="row justify-content-center align-items-center" style="height:100vh;"> <h2 class="h5 mb-3 fw-normal">Please sign in</h2>
<div class="col-md-4">
<div class="login-container card">
<div class="card-header text-center">
<img src="favicon.svg" alt="Logo" class="img-fluid" style="max-width: 100px;"> <!-- Adjust path and style as needed -->
<h4>Stirling-PDF Login</h4>
</div>
<div th:if="${logoutMessage}" class="alert alert-success" th:text="${logoutMessage}"></div>
<div class="form-floating">
<div class="card-body"> <input type="text" class="form-control" id="username"
<form th:action="@{/login}" method="post"> placeholder="admin"> <label for="username">Username</label>
<div class="form-group"> </div>
<label for="username">Username:</label> <div class="form-floating">
<input type="text" id="username" name="username" class="form-control" required="required" /> <input type="password" class="form-control" id="password"
</div> placeholder="Password"> <label for="password">Password</label>
<div class="form-group"> </div>
<label for="password">Password:</label>
<input type="password" id="password" name="password" class="form-control" required="required" />
</div>
<div class="form-group text-center">
<input type="submit" value="Login" class="btn btn-primary" />
</div>
</form>
</div>
<div class="card-footer text-danger text-center">
<div th:if="${error == 'badcredentials'}">
Invalid username or password.
</div>
<div th:if="${error == 'locked'}">
Your account has been locked.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
<div class="checkbox mb-3">
<label> <input type="checkbox" value="remember-me">
Remember me
</label>
</div>
<button class="w-100 btn btn-lg btn-primary" type="submit">Sign
in</button>
</form>
<div class="text-danger text-center">
<div th:if="${error == 'badcredentials'}">Invalid username or
password.</div>
<div th:if="${error == 'locked'}">Your account has been locked.
</div>
</div>
</main>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body> </body>
</html> </html>

View File

@ -14,15 +14,15 @@
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{merge.header}"></h2> <h2 th:text="#{merge.header}"></h2>
<form action="merge-pdfs" method="post" enctype="multipart/form-data"> <form action="merge-pdfs" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<label th:text="#{multiPdfDropPrompt}"></label> <label th:text="#{multiPdfDropPrompt}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=true, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=true, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<ul id="selectedFiles" class="list-group"></ul> <ul id="selectedFiles" class="list-group"></ul>
</div> </div>
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button> <button type="button" id="sortByNameBtn" class="btn btn-info" th:text="#{merge.sortByName}"></button>
<button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button> <button type="button" id="sortByDateBtn" class="btn btn-info" th:text="#{merge.sortByDate}"></button>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{merge.submit}"></button>

View File

@ -22,7 +22,7 @@
<div <div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<br> <br>
<div class="form-group"> <div class="mb-3">
<label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label> <select <label for="customMargin" th:text="#{addPageNumbers.selectText.2}"></label> <select
class="form-control" id="customMargin" name="customMargin" class="form-control" id="customMargin" name="customMargin"
required> required>
@ -77,7 +77,7 @@
</style> </style>
<div class="form-group"> <div class="mb-3">
<label for="position" th:text="#{addPageNumbers.selectText.3}"></label> <label for="position" th:text="#{addPageNumbers.selectText.3}"></label>
<div class="a4container"> <div class="a4container">
<div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div> <div class="pageNumber" id="1" style="top: 10%; left: 10%;">1</div>
@ -94,18 +94,18 @@
<input type="hidden" id="numberInput" name="position" min="1" <input type="hidden" id="numberInput" name="position" min="1"
max="9" required> max="9" required>
<div class="form-group"> <div class="mb-3">
<label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label> <input <label for="startingNumber" th:text="#{addPageNumbers.selectText.4}"></label> <input
type="number" class="form-control" id="startingNumber" type="number" class="form-control" id="startingNumber"
name="startingNumber" min="1" required value="1" /> name="startingNumber" min="1" required value="1" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label> <input <label for="pagesToNumber" th:text="#{addPageNumbers.selectText.5}"></label> <input
type="text" class="form-control" id="pagesToNumber" type="text" class="form-control" id="pagesToNumber"
name="pagesToNumber" name="pagesToNumber"
th:placeholder="#{addPageNumbers.numberPagesDesc}" /> th:placeholder="#{addPageNumbers.numberPagesDesc}" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="customText" th:text="#{addPageNumbers.selectText.6}"></label> <input type="text" <label for="customText" th:text="#{addPageNumbers.selectText.6}"></label> <input type="text"
class="form-control" id="customText" name="customText" class="form-control" id="customText" name="customText"
th:placeholder="#{addPageNumbers.customNumberDesc}" /> th:placeholder="#{addPageNumbers.customNumberDesc}" />

View File

@ -17,57 +17,57 @@
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p> <p class="text-muted" th:text="#{changeMetadata.selectText.1}"></p>
<div class="form-group-inline form-check"> <div class="mb-3-inline form-check">
<input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll"> <input type="checkbox" class="form-check-input" id="deleteAll" name="deleteAll">
<label class="ml-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label> <label class="ms-3" for="deleteAll" th:text="#{changeMetadata.selectText.2}" ></label>
</div> </div>
<div class="form-group-inline form-check"> <div class="mb-3-inline form-check">
<input type="checkbox" class="form-check-input" id="customModeCheckbox"> <input type="checkbox" class="form-check-input" id="customModeCheckbox">
<label class="ml-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label> <label class="ms-3" for="customModeCheckbox" th:text="#{changeMetadata.selectText.3}"></label>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label> <label class="form-check-label" for="author" th:text="#{changeMetadata.author}"></label>
<input type="text" class="form-control" id="author" name="author"> <input type="text" class="form-control" id="author" name="author">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label> <label class="form-check-label" for="creationDate" th:text="#{changeMetadata.creationDate}"></label>
<input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59"> <input type="text" class="form-control" id="creationDate" name="creationDate" placeholder="2020/12/25 18:30:59">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label> <label class="form-check-label" for="creator" th:text="#{changeMetadata.creator}"></label>
<input type="text" class="form-control" id="creator" name="creator"> <input type="text" class="form-control" id="creator" name="creator">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label> <label class="form-check-label" for="keywords" th:text="#{changeMetadata.keywords}"></label>
<input type="text" class="form-control" id="keywords" name="keywords"> <input type="text" class="form-control" id="keywords" name="keywords">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label> <label class="form-check-label" for="modificationDate" th:text="#{changeMetadata.modDate}"></label>
<input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59"> <input type="text" class="form-control" id="modificationDate" name="modificationDate" placeholder="2020/12/25 18:30:59">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label> <label class="form-check-label" for="producer" th:text="#{changeMetadata.producer}"></label>
<input type="text" class="form-control" id="producer" name="producer"> <input type="text" class="form-control" id="producer" name="producer">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label> <label class="form-check-label" for="subject" th:text="#{changeMetadata.subject}"></label>
<input type="text" class="form-control" id="subject" name="subject"> <input type="text" class="form-control" id="subject" name="subject">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label> <label class="form-check-label" for="title" th:text="#{changeMetadata.title}"></label>
<input type="text" class="form-control" id="title" name="title"> <input type="text" class="form-control" id="title" name="title">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label> <label class="form-check-label" for="trapped" th:text="#{changeMetadata.trapped}"></label>
<select class="form-control" id="trapped" name="trapped"> <select class="form-control" id="trapped" name="trapped">
<option value="True" th:text="#{true}"></option> <option value="True" th:text="#{true}"></option>
@ -77,7 +77,7 @@
</div> </div>
<div id="customMetadata" style="display: none;"> <div id="customMetadata" style="display: none;">
<h3 th:text="#{changeMetadata.selectText.4}"></h3> <h3 th:text="#{changeMetadata.selectText.4}"></h3>
<div class="form-group" id="otherMetadataEntries"></div> <div class="mb-3" id="otherMetadataEntries"></div>
</div> </div>
<div id="customMetadataEntries"></div> <div id="customMetadataEntries"></div>
<button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button> <button type="button" class="btn btn-secondary" id="addMetadataBtn" th:text="#{changeMetadata.selectText.5}"></button>
@ -194,7 +194,7 @@
count = count + 1; count = count + 1;
const formGroup = document.createElement("div"); const formGroup = document.createElement("div");
formGroup.className = "form-group"; formGroup.className = "mb-3";
formGroup.appendChild(keyInput); formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput); formGroup.appendChild(valueInput);
@ -227,11 +227,11 @@
continue; continue;
} }
const entryDiv = document.createElement('div'); const entryDiv = document.createElement('div');
entryDiv.className = 'form-group'; entryDiv.className = 'mb-3';
entryDiv.innerHTML = `<div class="form-group"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`; entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv); otherMetadataEntriesDiv.appendChild(entryDiv);
} }
} else { } else {

View File

@ -16,27 +16,27 @@
<form id="multiPdfForm" th:action="@{extract-image-scans}" method="post" enctype="multipart/form-data"> <form id="multiPdfForm" th:action="@{extract-image-scans}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*, application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='image/*, application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label> <label for="angleThreshold" th:text="#{ScannerImageSplit.selectText.1}"></label>
<input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10"> <input type="number" class="form-control" id="angleThreshold" name="angle_threshold" value="10">
<small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small> <small id="angleThresholdHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.2}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label> <label for="tolerance" th:text="#{ScannerImageSplit.selectText.3}"></label>
<input type="number" class="form-control" id="tolerance" name="tolerance" value="20"> <input type="number" class="form-control" id="tolerance" name="tolerance" value="20">
<small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small> <small id="toleranceHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.4}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label> <label for="minArea" th:text="#{ScannerImageSplit.selectText.5}"></label>
<input type="number" class="form-control" id="minArea" name="min_area" value="8000"> <input type="number" class="form-control" id="minArea" name="min_area" value="8000">
<small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small> <small id="minAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.6}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label> <label for="minContourArea" th:text="#{ScannerImageSplit.selectText.7}"></label>
<input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500"> <input type="number" class="form-control" id="minContourArea" name="min_contour_area" value="500">
<small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small> <small id="minContourAreaHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.8}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label> <label for="borderSize" th:text="#{ScannerImageSplit.selectText.9}"></label>
<input type="number" class="form-control" id="borderSize" name="border_size" value="1"> <input type="number" class="form-control" id="borderSize" name="border_size" value="1">
<small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small> <small id="borderSizeHelp" class="form-text text-muted" th:text="#{ScannerImageSplit.selectText.10}"></small>

View File

@ -16,7 +16,7 @@
<form id="multiPdfForm" th:action="@{extract-images}" method="post" enctype="multipart/form-data"> <form id="multiPdfForm" th:action="@{extract-images}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{extractImages.selectText}"></label> <label th:text="#{extractImages.selectText}"></label>
<select class="form-control" name="format"> <select class="form-control" name="format">
<option value="png">PNG</option> <option value="png">PNG</option>

View File

@ -16,7 +16,7 @@
<h2 th:text="#{pageLayout.header}"></h2> <h2 th:text="#{pageLayout.header}"></h2>
<form id="multiPdfForm" th:action="@{multi-page-layout}" method="post" enctype="multipart/form-data"> <form id="multiPdfForm" th:action="@{multi-page-layout}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="pagesPerSheet" th:text="#{pageLayout.pagesPerSheet}"></label> <label for="pagesPerSheet" th:text="#{pageLayout.pagesPerSheet}"></label>
<select id="pagesPerSheet" name="pagesPerSheet" required> <select id="pagesPerSheet" name="pagesPerSheet" required>
<option value="2">2</option> <option value="2">2</option>

View File

@ -40,7 +40,7 @@
<h2 th:text="#{ocr.header}"></h2> <h2 th:text="#{ocr.header}"></h2>
<form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{/ocr-pdf}" method="post" enctype="multipart/form-data" class="mb-3"> <form th:if="${#lists.size(languages) > 0}" action="#" th:action="@{/ocr-pdf}" method="post" enctype="multipart/form-data" class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label> <label for="languages" class="form-label" th:text="#{ocr.selectText.1}"></label>
<hr> <hr>
<div id="languages"> <div id="languages">
@ -51,7 +51,7 @@
</div> </div>
<hr> <hr>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{ocr.selectText.10}"></label> <label th:text="#{ocr.selectText.10}"></label>
<select class="form-control" name="ocrType"> <select class="form-control" name="ocrType">
<option value="skip-text" th:text="#{ocr.selectText.6}"></option> <option value="skip-text" th:text="#{ocr.selectText.6}"></option>
@ -83,7 +83,7 @@
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{ocr.selectText.12}"></label> <label th:text="#{ocr.selectText.12}"></label>
<select class="form-control" name="ocrRenderType"> <select class="form-control" name="ocrRenderType">
<option value="hocr">HOCR (Latin/Roman alphabet only)</option> <option value="hocr">HOCR (Latin/Roman alphabet only)</option>

View File

@ -16,12 +16,12 @@
<form id="multiPdfForm" th:action="@{remove-blanks}" method="post" enctype="multipart/form-data"> <form id="multiPdfForm" th:action="@{remove-blanks}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="threshold" th:text="#{removeBlanks.threshold}"></label> <label for="threshold" th:text="#{removeBlanks.threshold}"></label>
<input type="number" class="form-control" id="threshold" name="threshold" value="10"> <input type="number" class="form-control" id="threshold" name="threshold" value="10">
<small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small> <small id="thresholdHelp" class="form-text text-muted" th:text="#{removeBlanks.thresholdDesc}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label> <label for="whitePercent" th:text="#{removeBlanks.whitePercent}"></label>
<input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1"> <input type="number" class="form-control" id="whitePercent" name="whitePercent" value="99.9" step="0.1">
<small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small> <small id="whitePercentHelp" class="form-text text-muted" th:text="#{removeBlanks.whitePercentDesc}"></small>

View File

@ -16,7 +16,7 @@
<h2 th:text="#{scalePages.header}"></h2> <h2 th:text="#{scalePages.header}"></h2>
<form id="scalePagesFrom" th:action="@{scale-pages}" method="post" enctype="multipart/form-data"> <form id="scalePagesFrom" th:action="@{scale-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="pageSize" th:text="#{scalePages.pageSize}"></label> <label for="pageSize" th:text="#{scalePages.pageSize}"></label>
<select id="pageSize" name="pageSize" required> <select id="pageSize" name="pageSize" required>
@ -48,7 +48,7 @@
<option value="LEDGER">Ledger</option> <option value="LEDGER">Ledger</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="scaleFactor" th:text="#{scalePages.scaleFactor}"></label> <label for="scaleFactor" th:text="#{scalePages.scaleFactor}"></label>
<input type="number" id="scaleFactor" name="scaleFactor" step="any" min="0" value="1"> <input type="number" id="scaleFactor" name="scaleFactor" step="any" min="0" value="1">
</div> </div>

View File

@ -16,7 +16,7 @@
<form th:action="@{rearrange-pages}" method="post" enctype="multipart/form-data"> <form th:action="@{rearrange-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="customMode">Mode</label> <label for="customMode">Mode</label>
<select class="form-control" id="customMode" name="customMode"> <select class="form-control" id="customMode" name="customMode">
<option value="">Custom Page Order</option> <option value="">Custom Page Order</option>
@ -29,7 +29,7 @@
<option value="REMOVE_FIRST_AND_LAST">Remove First and Last</option> <option value="REMOVE_FIRST_AND_LAST">Remove First and Last</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="pageOrder" th:text="#{pageOrderPrompt}"></label> <label for="pageOrder" th:text="#{pageOrderPrompt}"></label>
<input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" required> <input type="text" class="form-control" id="pageOrder" name="pageOrder" placeholder="(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)" required>
</div> </div>

View File

@ -37,11 +37,11 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="bordered-box"> <div class="bordered-box">
<div class="text-right text-top"> <div class="text-end text-top">
<button id="uploadPipelineBtn" class="btn btn-primary">Upload <button id="uploadPipelineBtn" class="btn btn-primary">Upload
Custom</button> Custom</button>
<button type="button" class="btn btn-primary" data-toggle="modal" <button type="button" class="btn btn-primary" data-bs-toggle="modal"
data-target="#pipelineSettingsModal">Configure</button> data-bs-target="#pipelineSettingsModal">Configure</button>
</div> </div>
<div class="center-element"> <div class="center-element">
@ -71,7 +71,7 @@
<!-- Modal Header --> <!-- Modal Header -->
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Pipeline Configuration</h2> <h2 class="modal-title">Pipeline Configuration</h2>
<button type="button" class="close" data-dismiss="modal">&times;</button> <button type="button" class="close" data-bs-dismiss="modal">&times;</button>
</div> </div>
<!-- Modal body --> <!-- Modal body -->

View File

@ -16,7 +16,7 @@
<form th:action="@{remove-pages}" method="post" enctype="multipart/form-data"> <form th:action="@{remove-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="pagesToDelete" th:text="#{pageRemover.pagesToDelete}"></label> <label for="pagesToDelete" th:text="#{pageRemover.pagesToDelete}"></label>
<input type="text" class="form-control" id="fileInput" name="pagesToDelete" placeholder="(e.g. 1,2,6 or 1-10,15-30)" required> <input type="text" class="form-control" id="fileInput" name="pagesToDelete" placeholder="(e.g. 1,2,6 or 1-10,15-30)" required>
</div> </div>

View File

@ -14,27 +14,27 @@
<h2 th:text="#{addPassword.header}"></h2> <h2 th:text="#{addPassword.header}"></h2>
<form action="add-password" method="post" enctype="multipart/form-data"> <form action="add-password" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<label th:text="#{addPassword.selectText.1}"></label> <label th:text="#{addPassword.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{addPassword.selectText.14}"></label> <input type="password" class="form-control" id="ownerPassword" name="ownerPassword"> <label th:text="#{addPassword.selectText.14}"></label> <input type="password" class="form-control" id="ownerPassword" name="ownerPassword">
<small class="form-text text-muted" th:text="#{addPassword.selectText.15}"></small> <small class="form-text text-muted" th:text="#{addPassword.selectText.15}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{addPassword.selectText.2}"></label> <input type="password" class="form-control" id="password" name="password"> <label th:text="#{addPassword.selectText.2}"></label> <input type="password" class="form-control" id="password" name="password">
<small class="form-text text-muted" th:text="#{addPassword.selectText.16}"></small> <small class="form-text text-muted" th:text="#{addPassword.selectText.16}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{addPassword.selectText.3}"></label> <select class="form-control" id="keyLength" name="keyLength"> <label th:text="#{addPassword.selectText.3}"></label> <select class="form-control" id="keyLength" name="keyLength">
<option value="40">40</option> <option value="40">40</option>
<option value="128">128</option> <option value="128">128</option>
<option value="256">256</option> <option value="256">256</option>
</select> <small class="form-text text-muted" th:text="#{addPassword.selectText.4}"></small> </select> <small class="form-text text-muted" th:text="#{addPassword.selectText.4}"></small>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{addPassword.selectText.5}"></label> <label th:text="#{addPassword.selectText.5}"></label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="canAssembleDocument" name="canAssembleDocument"> <input class="form-check-input" type="checkbox" id="canAssembleDocument" name="canAssembleDocument">
@ -71,7 +71,7 @@
</div> </div>
<br /> <br />
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPassword.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{addPassword.submit}"></button>
</div> </div>

View File

@ -14,14 +14,14 @@
<h2 th:text="#{watermark.header}"></h2> <h2 th:text="#{watermark.header}"></h2>
<form method="post" enctype="multipart/form-data" action="add-watermark"> <form method="post" enctype="multipart/form-data" action="add-watermark">
<div class="form-group"> <div class="mb-3">
<label th:text="#{watermark.selectText.1}"></label> <label th:text="#{watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}">
<input type="file" id="fileInput" name="fileInput" class="form-control-file" accept="application/pdf" required /> <input type="file" id="fileInput" name="fileInput" class="form-control-file" accept="application/pdf" required />
</div> </div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{watermark.selectText.8}"></label> <label th:text="#{watermark.selectText.8}"></label>
<select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()"> <select class="form-control" id="watermarkType" name="watermarkType" onchange="toggleFileOption()">
<option value="text">Text</option> <option value="text">Text</option>
@ -29,21 +29,21 @@
</select> </select>
</div> </div>
<div id="watermarkTextGroup" class="form-group"> <div id="watermarkTextGroup" class="mb-3">
<label for="watermarkText" th:text="#{watermark.selectText.2}"></label> <label for="watermarkText" th:text="#{watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
</div> </div>
<div id="watermarkImageGroup" class="form-group" style="display: none;"> <div id="watermarkImageGroup" class="mb-3" style="display: none;">
<label for="watermarkImage" th:text="#{watermark.selectText.9}"></label> <label for="watermarkImage" th:text="#{watermark.selectText.9}"></label>
<input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*" /> <input type="file" id="watermarkImage" name="watermarkImage" class="form-control-file" accept="image/*" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="fontSize" th:text="#{watermark.selectText.3}"></label> <label for="fontSize" th:text="#{watermark.selectText.3}"></label>
<input type="text" id="fontSize" name="fontSize" class="form-control" value="30" /> <input type="text" id="fontSize" name="fontSize" class="form-control" value="30" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="opacity" th:text="#{watermark.selectText.7}"></label> <label for="opacity" th:text="#{watermark.selectText.7}"></label>
<input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()" /> <input type="text" id="opacity" name="opacityText" class="form-control" value="50" onblur="updateOpacityValue()" />
<input type="hidden" id="opacityReal" name="opacity" value="0.5"> <input type="hidden" id="opacityReal" name="opacity" value="0.5">
@ -83,19 +83,19 @@
appendPercentageSymbol(); appendPercentageSymbol();
</script> </script>
<div class="form-group"> <div class="mb-3">
<label for="rotation" th:text="#{watermark.selectText.4}"></label> <label for="rotation" th:text="#{watermark.selectText.4}"></label>
<input type="text" id="rotation" name="rotation" class="form-control" value="45" /> <input type="text" id="rotation" name="rotation" class="form-control" value="45" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="widthSpacer" th:text="#{watermark.selectText.5}"></label> <label for="widthSpacer" th:text="#{watermark.selectText.5}"></label>
<input type="text" id="widthSpacer" name="widthSpacer" class="form-control" value="50" /> <input type="text" id="widthSpacer" name="widthSpacer" class="form-control" value="50" />
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="heightSpacer" th:text="#{watermark.selectText.6}"></label> <label for="heightSpacer" th:text="#{watermark.selectText.6}"></label>
<input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50" /> <input type="text" id="heightSpacer" name="heightSpacer" class="form-control" value="50" />
</div> </div>
<div class="form-group text-center"> <div class="mb-3 text-center">
<input type="submit" id="submitBtn" th:value="#{watermark.submit}" class="btn btn-primary" /> <input type="submit" id="submitBtn" th:value="#{watermark.submit}" class="btn btn-primary" />
</div> </div>
</form> </form>

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
<th:block th:insert="~{fragments/common :: head(title=#{autoRedact.title})}"></th:block>
<body>
<div id="page-container">
<div id="content-wrap">
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
<br> <br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6">
<h2 th:text="#{autoRedact.header}"></h2>
<form action="/auto-redact" method="post" enctype="multipart/form-data">
<div class="mb-3">
<input type="file" class="form-control" id="fileInput" name="fileInput" required accept="application/pdf">
</div>
<div class="mb-3">
<label for="listOfText" class="form-label" th:text="#{autoRedact.textsToRedactLabel}"></label>
<textarea class="form-control" id="listOfText" name="listOfText" rows="4" required th:placeholder="#{autoRedact.textsToRedactPlaceholder}"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="useRegex" name="useRegex">
<label class="form-check-label" for="useRegex" th:text="#{autoRedact.useRegexLabel}"></label>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="wholeWordSearch" name="wholeWordSearch">
<label class="form-check-label" for="wholeWordSearch" th:text="#{autoRedact.wholeWordSearchLabel}"></label>
</div>
<div class="mb-3">
<label for="customPadding" class="form-label" th:text="#{autoRedact.customPaddingLabel}"></label>
<input type="number" step="0.1" class="form-control" id="customPadding" name="customPadding" value="0.1">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="convertPDFToImage" name="convertPDFToImage" checked>
<label class="form-check-label" for="convertPDFToImage" th:text="#{autoRedact.convertPDFToImageLabel}"></label>
</div>
<button type="submit" class="btn btn-primary" th:text="#{autoRedact.submitButton}"></button>
</form>
</div>
</div>
</div>
</div>
<div th:insert="~{fragments/footer.html :: footer}"></div>
</div>
</body>
</html>

View File

@ -18,12 +18,12 @@
<form action="/cert-sign" method="post" <form action="/cert-sign" method="post"
enctype="multipart/form-data"> enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<label th:text="#{certSign.selectPDF}"></label> <label th:text="#{certSign.selectPDF}"></label>
<div <div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="certType" th:text="#{certSign.certType}"></label> <select <label for="certType" th:text="#{certSign.certType}"></label> <select
class="form-control" id="certType" name="certType"> class="form-control" id="certType" name="certType">
<option value="" th:text="#{selectFillter}"></option> <option value="" th:text="#{selectFillter}"></option>
@ -32,50 +32,50 @@
</select> </select>
</div> </div>
<div class="form-group" id="p12Group" style="display: none;"> <div class="mb-3" id="p12Group" style="display: none;">
<label th:text="#{certSign.selectP12}"></label> <label th:text="#{certSign.selectP12}"></label>
<div <div
th:replace="~{fragments/common :: fileSelector(name='p12', notRequired=true, multiple=false, accept='.p12,.pfx')}"></div> th:replace="~{fragments/common :: fileSelector(name='p12', notRequired=true, multiple=false, accept='.p12,.pfx')}"></div>
</div> </div>
<div id="pemGroup" style="display: none;"> <div id="pemGroup" style="display: none;">
<div class="form-group"> <div class="mb-3">
<label th:text="#{certSign.selectKey}"></label> <label th:text="#{certSign.selectKey}"></label>
<div <div
th:replace="~{fragments/common :: fileSelector(name='key', multiple=false, notRequired=true, accept='.pem,.der')}"></div> th:replace="~{fragments/common :: fileSelector(name='key', multiple=false, notRequired=true, accept='.pem,.der')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{certSign.selectCert}"></label> <label th:text="#{certSign.selectCert}"></label>
<div <div
th:replace="~{fragments/common :: fileSelector(name='cert', multiple=false, notRequired=true, accept='.pem,.der')}"></div> th:replace="~{fragments/common :: fileSelector(name='cert', multiple=false, notRequired=true, accept='.pem,.der')}"></div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{certSign.password}"></label> <input <label th:text="#{certSign.password}"></label> <input
type="password" class="form-control" id="password" type="password" class="form-control" id="password"
name="password"> name="password">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label><input type="checkbox" id="showSignature" <label><input type="checkbox" id="showSignature"
name="showSignature" th:text="#{certSign.showSig}"></label> name="showSignature" th:text="#{certSign.showSig}"></label>
</div> </div>
<div id="signatureDetails" style="display: none;"> <div id="signatureDetails" style="display: none;">
<div class="form-group"> <div class="mb-3">
<label for="reason" th:text="#{certSign.reason}"></label> <input type="text" <label for="reason" th:text="#{certSign.reason}"></label> <input type="text"
class="form-control" id="reason" name="reason"> class="form-control" id="reason" name="reason">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="location" th:text="#{certSign.location}"></label> <input type="text" <label for="location" th:text="#{certSign.location}"></label> <input type="text"
class="form-control" id="location" name="location"> class="form-control" id="location" name="location">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="name" th:text="#{certSign.name}"></label> <input type="text" <label for="name" th:text="#{certSign.name}"></label> <input type="text"
class="form-control" id="name" name="name"> class="form-control" id="name" name="name">
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="pageNumber" th:text="#{pageNum}"></label> <input <label for="pageNumber" th:text="#{pageNum}"></label> <input
type="number" class="form-control" id="pageNumber" type="number" class="form-control" id="pageNumber"
name="pageNumber" min="1"> name="pageNumber" min="1">
@ -120,7 +120,7 @@
</script> </script>
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" <button type="submit" id="submitBtn" class="btn btn-primary"
th:text="#{certSign.submit}"></button> th:text="#{certSign.submit}"></button>
</div> </div>

View File

@ -15,11 +15,11 @@
<h2 th:text="#{permissions.header}"></h2> <h2 th:text="#{permissions.header}"></h2>
<p th:text="#{permissions.warning}"></p> <p th:text="#{permissions.warning}"></p>
<form action="add-password" method="post" enctype="multipart/form-data"> <form action="add-password" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<label th:text="#{permissions.selectText.1}"></label> <label th:text="#{permissions.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{permissions.selectText.2}"></label> <label th:text="#{permissions.selectText.2}"></label>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="canAssembleDocument" name="canAssembleDocument"> <input class="form-check-input" type="checkbox" id="canAssembleDocument" name="canAssembleDocument">
@ -56,7 +56,7 @@
</div> </div>
<br /> <br />
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{permissions.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{permissions.submit}"></button>
</div> </div>

View File

@ -142,13 +142,17 @@
const buttonElem = document.createElement('button'); const buttonElem = document.createElement('button');
buttonElem.className = 'btn btn-link'; buttonElem.className = 'btn btn-link';
buttonElem.type = 'button'; buttonElem.type = 'button';
buttonElem.dataset.toggle = "collapse"; buttonElem.dataset.bsToggle = "collapse";
buttonElem.dataset.target = `#${safeKey}-content-${depth}`; buttonElem.dataset.bsTarget = `#${safeKey}-content-${depth}`;
buttonElem.setAttribute('aria-expanded', 'true'); buttonElem.setAttribute('aria-expanded', 'true');
buttonElem.setAttribute('aria-controls', `${safeKey}-content-${depth}`); buttonElem.setAttribute('aria-controls', `${safeKey}-content-${depth}`);
buttonElem.textContent = key; buttonElem.textContent = key;
return buttonElem; return buttonElem;
} }
const collapsibleElems = document.querySelectorAll('[data-bs-toggle="collapse"]');
collapsibleElems.forEach(elem => {
new bootstrap.Collapse(elem);
});
</script> </script>
</div> </div>
</div> </div>

View File

@ -14,16 +14,16 @@
<h2 th:text="#{removePassword.header}"></h2> <h2 th:text="#{removePassword.header}"></h2>
<form action="remove-password" method="post" enctype="multipart/form-data"> <form action="remove-password" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<label th:text="#{removePassword.selectText.1}"></label> <label th:text="#{removePassword.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label th:text="#{removePassword.selectText.2}"></label> <label th:text="#{removePassword.selectText.2}"></label>
<input type="password" class="form-control" id="password" name="password" required> <input type="password" class="form-control" id="password" name="password" required>
</div> </div>
<br /> <br />
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removePassword.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{removePassword.submit}"></button>
</div> </div>
</form> </form>

View File

@ -14,15 +14,15 @@
<h2 th:text="#{remove-watermark.header}"></h2> <h2 th:text="#{remove-watermark.header}"></h2>
<form method="post" enctype="multipart/form-data" action="remove-watermark"> <form method="post" enctype="multipart/form-data" action="remove-watermark">
<div class="form-group"> <div class="mb-3">
<label th:text="#{remove-watermark.selectText.1}"></label> <label th:text="#{remove-watermark.selectText.1}"></label>
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-group"> <div class="mb-3">
<label for="watermarkText" th:text="#{remove-watermark.selectText.2}"></label> <label for="watermarkText" th:text="#{remove-watermark.selectText.2}"></label>
<input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required /> <input type="text" id="watermarkText" name="watermarkText" class="form-control" placeholder="Stirling-PDF" required />
</div> </div>
<div class="form-group text-center"> <div class="mb-3 text-center">
<input type="submit" id="submitBtn" th:value="#{remove-watermark.submit}" class="btn btn-primary" /> <input type="submit" id="submitBtn" th:value="#{remove-watermark.submit}" class="btn btn-primary" />
</div> </div>
</form> </form>

View File

@ -14,7 +14,7 @@
<h2 th:text="#{sanitizePDF.header}"></h2> <h2 th:text="#{sanitizePDF.header}"></h2>
<form action="sanitize-pdf" method="post" enctype="multipart/form-data"> <form action="sanitize-pdf" method="post" enctype="multipart/form-data">
<div class="form-group"> <div class="mb-3">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
</div> </div>
<div class="form-check"> <div class="form-check">
@ -38,7 +38,7 @@
<label class="form-check-label" for="removeFonts" th:text="#{sanitizePDF.selectText.5}"></label> <label class="form-check-label" for="removeFonts" th:text="#{sanitizePDF.selectText.5}"></label>
</div> </div>
<br /> <br />
<div class="form-group text-center"> <div class="mb-3 text-center">
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{sanitizePDF.submit}"></button> <button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{sanitizePDF.submit}"></button>
</div> </div>

View File

@ -26,7 +26,7 @@
<form th:action="@{split-pages}" method="post" enctype="multipart/form-data"> <form th:action="@{split-pages}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<div class="form-group"> <div class="mb-3">
<label for="pages" th:text="#{split.splitPages}"></label> <label for="pages" th:text="#{split.splitPages}"></label>
<input type="text" class="form-control" id="pages" name="pages" placeholder="1,3,5-10" required> <input type="text" class="form-control" id="pages" name="pages" placeholder="1,3,5-10" required>
</div> </div>