Compare commits

...

39 Commits

Author SHA1 Message Date
Anthony Stirling
6119181276 fixes 2024-12-24 09:43:45 +00:00
Anthony Stirling
bf9a00868d fixes 2024-12-24 09:36:48 +00:00
Anthony Stirling
a58b4d2286 formattingand autowired constructors 2024-12-24 09:09:51 +00:00
Anthony Stirling
a71cb7f5d9 Merge pull request #2509 from Ludy87/btn_standardize
standardize the submit button
2024-12-20 10:24:49 +00:00
Ludy87
a9e250547a standardize the submit button 2024-12-20 11:08:57 +01:00
Anthony Stirling
30bcdbd7be Merge pull request #2507 from Ludy87/remove_redundant_entries
remove redundant entries
2024-12-20 09:13:28 +00:00
Ludy87
61603aebb2 remove redundant entries 2024-12-20 09:59:12 +01:00
Anthony Stirling
8dca4a588d Merge pull request #2505 from reecebrowne/toolbar-tweaks2
Toolbar tweaks2
2024-12-19 13:22:41 +00:00
Reece Browne
63386baa0d Conditional logic for stacking columns 2024-12-19 12:58:57 +00:00
Reece Browne
2a93910da3 Remove padding 2024-12-19 12:04:08 +00:00
Reece Browne
fca6dc1fd8 Stack convert section, don't remove. Move remove into own js 2024-12-19 11:59:58 +00:00
Anthony Stirling
2d82c5fa77 Merge pull request #2503 from omar-ahmed42/fix-page-resize-in-redact-img-conversion
Fix: Draw image with the original PDF page dimensions
2024-12-19 11:49:18 +00:00
Omar Ahmed Hassan
167c792bf0 Draw image with the original PDF page dimensions 2024-12-19 13:34:13 +02:00
Anthony Stirling
95a9e10dc8 Update releaseArtifacts.yml 2024-12-19 10:52:41 +00:00
Anthony Stirling
4d6368048c Update build.gradle 2024-12-18 21:28:23 +00:00
Anthony Stirling
4e715a82e0 Merge pull request #2501 from Ludy87/missing_pdflib
[Bugfix] A variety of tools miss PDFLib
2024-12-18 21:12:08 +00:00
Ludy87
164381e940 some tools lack PDFLib 2024-12-18 21:03:08 +01:00
Anthony Stirling
0436f45de5 Merge pull request #2500 from Stirling-Tools/configCheck
Config mount check
2024-12-18 18:14:46 +00:00
Anthony Stirling
6ce761aff2 Merge remote-tracking branch 'origin/main' into configCheck 2024-12-18 18:05:16 +00:00
Anthony Stirling
8a5d9f9a95 mounted_config_dir 2024-12-18 18:04:10 +00:00
Anthony Stirling
c0ef624a1d Merge pull request #2499 from albanobattistella/patch-62
Update messages_it_IT.properties
2024-12-18 14:42:48 +00:00
albanobattistella
b9ae90274f Update messages_it_IT.properties 2024-12-18 15:34:39 +01:00
Anthony Stirling
764b8f4d22 Merge pull request #2498 from Ludy87/code_refactoring
Exclude Sensitive and Error-Prone Fields from toString() in SAML2 Con…
2024-12-18 12:58:13 +00:00
Ludy87
a531f53893 Exclude Sensitive and Error-Prone Fields from toString() in SAML2 Configuration 2024-12-18 13:40:24 +01:00
Anthony Stirling
74d6d96f4e Merge pull request #2495 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-18 12:18:17 +00:00
Anthony Stirling
1862ab1671 Merge pull request #2497 from reecebrowne/File-input-append
Remove file input append
2024-12-18 12:18:02 +00:00
Reece Browne
bf95ca43dc Remove append entirely 2024-12-18 11:47:23 +00:00
Reece Browne
678ef85da1 Don't remove files from file input, just append 2024-12-18 11:09:52 +00:00
Anthony Stirling
9a6afdd921 Merge pull request #2493 from reecebrowne/bug/2490/2488/image-to-pdf
Img to pdf bug fixes
2024-12-18 10:42:34 +00:00
reecebrowne
a72615cc86 Merge branch 'main' into bug/2490/2488/image-to-pdf 2024-12-18 10:40:54 +00:00
Anthony Stirling
b0daac2566 Merge pull request #2492 from omar-ahmed42/fix-no-formdata-sent-in-multi
Fix: form data not being sent in multi
2024-12-18 10:37:31 +00:00
github-actions[bot]
2335ecf017 📝 Sync README
> Made via sync_files.yml
2024-12-18 10:34:08 +00:00
Anthony Stirling
a79318bf0c Merge pull request #2491 from Ludy87/database_create_m
Adds manual backup creation
2024-12-18 10:33:50 +00:00
Reece Browne
9eed761346 Correct default fit 2024-12-18 00:36:04 +00:00
Reece Browne
12d86049f6 Add default to convert image to pdf api 2024-12-18 00:30:06 +00:00
Reece Browne
e9f80d03ea Fix missing info in form data 2024-12-18 00:16:01 +00:00
Omar Ahmed Hassan
42c238d87d Pass form to submitMultiPdfForm
- Pass form to submitMultiPdfForm to use in FormData creation.
2024-12-18 01:28:04 +02:00
Ludy87
051cd2d0d5 Adds manual backup creation 2024-12-17 23:23:16 +01:00
a
faf3454a02 Merge remote-tracking branch 'origin/main' into csrf2 2024-12-14 10:42:26 +00:00
165 changed files with 1593 additions and 1709 deletions

View File

@@ -74,43 +74,3 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
push-ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.7
- name: Generate exe
run: ./gradlew clean createExe
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: true
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Upload Assets binarie
uses: actions/upload-artifact@v4
with:
path: ./build/launch4j/Stirling-PDF.exe
name: Stirling-PDF.exe
overwrite: true
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./build/launch4j/Stirling-PDF.exe

View File

@@ -1,12 +1,5 @@
# New Database Backup and Import Functionality # New Database Backup and Import Functionality
> [!IMPORTANT]
> **Full activation will take place on approximately January 5th, 2025!**
Why is the waiting time six months?
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
## Functionality Overview ## Functionality Overview
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works: The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:

View File

@@ -192,13 +192,13 @@ Stirling-PDF currently supports 38 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) | | Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![92%](https://geps.dev/progress/92) |
| Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) | | Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) |
| Bulgarian (Български) (bg_BG) | ![90%](https://geps.dev/progress/90) | | Bulgarian (Български) (bg_BG) | ![89%](https://geps.dev/progress/89) |
| Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) | | Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) |
| Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) | | Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) |
| Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) | | Czech (Česky) (cs_CZ) | ![90%](https://geps.dev/progress/90) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) | | Danish (Dansk) (da_DK) | ![89%](https://geps.dev/progress/89) |
| Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) | | Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
@@ -207,17 +207,17 @@ Stirling-PDF currently supports 38 languages!
| Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) | | Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) |
| Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) | | Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) |
| Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) | | Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![91%](https://geps.dev/progress/91) | | Indonesian (Bahasa Indonesia) (id_ID) | ![90%](https://geps.dev/progress/90) |
| Irish (Gaeilge) (ga_IE) | ![83%](https://geps.dev/progress/83) | | Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | | Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) | | Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) | | Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) |
| Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) | | Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) |
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) | | Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) | | Portuguese (Português) (pt_PT) | ![90%](https://geps.dev/progress/90) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) | | Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![85%](https://geps.dev/progress/85) | | Romanian (Română) (ro_RO) | ![84%](https://geps.dev/progress/84) |
| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) | | Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) | | Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |

View File

@@ -27,7 +27,7 @@ ext {
} }
group = "stirling.software" group = "stirling.software"
version = "0.36.4" version = "0.36.5"
java { java {
@@ -38,10 +38,6 @@ java {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven {
url 'https://build.shibboleth.net/maven/releases'
}
maven { url "https://build.shibboleth.net/maven/releases" } maven { url "https://build.shibboleth.net/maven/releases" }
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" } maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" }

View File

@@ -97,14 +97,14 @@ public abstract class CreateSignatureBase implements SignatureInterface {
this.privateKey = privateKey; this.privateKey = privateKey;
} }
public final void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain;
}
public Certificate[] getCertificateChain() { public Certificate[] getCertificateChain() {
return certificateChain; return certificateChain;
} }
public final void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain;
}
public void setTsaUrl(String tsaUrl) { public void setTsaUrl(String tsaUrl) {
this.tsaUrl = tsaUrl; this.tsaUrl = tsaUrl;
} }
@@ -152,6 +152,10 @@ public abstract class CreateSignatureBase implements SignatureInterface {
} }
} }
public boolean isExternalSigning() {
return externalSigning;
}
/** /**
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would * Set if external signing scenario should be used. If {@code false}, SignatureInterface would
* be used for signing. * be used for signing.
@@ -163,8 +167,4 @@ public abstract class CreateSignatureBase implements SignatureInterface {
public void setExternalSigning(boolean externalSigning) { public void setExternalSigning(boolean externalSigning) {
this.externalSigning = externalSigning; this.externalSigning = externalSigning;
} }
public boolean isExternalSigning() {
return externalSigning;
}
} }

View File

@@ -51,15 +51,13 @@ public class TSAClient {
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER = private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
new DefaultDigestAlgorithmIdentifierFinder(); new DefaultDigestAlgorithmIdentifierFinder();
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
private final URL url; private final URL url;
private final String username; private final String username;
private final String password; private final String password;
private final MessageDigest digest; private final MessageDigest digest;
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
/** /**
* @param url the URL of the TSA service * @param url the URL of the TSA service
* @param username user name of TSA * @param username user name of TSA

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.EE; package stirling.software.SPDF.EE;
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.core.Ordered; import org.springframework.core.Ordered;
@@ -14,8 +13,15 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class EEAppConfig { public class EEAppConfig {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired private LicenseKeyChecker licenseKeyChecker;
private final LicenseKeyChecker licenseKeyChecker;
public EEAppConfig(
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker;
}
@Bean(name = "runningEE") @Bean(name = "runningEE")
public boolean runningEnterpriseEdition() { public boolean runningEnterpriseEdition() {

View File

@@ -17,18 +17,16 @@ public class LibreOfficeListener {
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener(); private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
private static final int LISTENER_PORT = 2002; private static final int LISTENER_PORT = 2002;
private ExecutorService executorService;
private long lastActivityTime;
private Process process;
private LibreOfficeListener() {}
public static LibreOfficeListener getInstance() { public static LibreOfficeListener getInstance() {
return INSTANCE; return INSTANCE;
} }
private ExecutorService executorService;
private long lastActivityTime;
private Process process;
private LibreOfficeListener() {}
private boolean isListenerRunning() { private boolean isListenerRunning() {
log.info("waiting for listener to start"); log.info("waiting for listener to start");
try (Socket socket = new Socket()) { try (Socket socket = new Socket()) {

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.file.Files; import java.nio.file.Files;
@@ -11,8 +10,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import javax.swing.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@@ -34,24 +31,22 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class SPdfApplication { public class SPdfApplication {
@Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties;
private static String baseUrlStatic; private static String baseUrlStatic;
private static String serverPortStatic; private static String serverPortStatic;
private final Environment env;
private final ApplicationProperties applicationProperties;
private final WebBrowser webBrowser;
@Value("${baseUrl:http://localhost}") @Value("${baseUrl:http://localhost}")
private String baseUrl; private String baseUrl;
@Value("${server.port:8080}") public SPdfApplication(
public void setServerPortStatic(String port) { Environment env,
if ("auto".equalsIgnoreCase(port)) { ApplicationProperties applicationProperties,
// Use Spring Boot's automatic port assignment (server.port=0) @Autowired(required = false) WebBrowser webBrowser) {
SPdfApplication.serverPortStatic = this.env = env;
"0"; // This will let Spring Boot assign an available port this.applicationProperties = applicationProperties;
} else { this.webBrowser = webBrowser;
SPdfApplication.serverPortStatic = port;
}
} }
// Optionally keep this method if you want to provide a manual port-incrementation fallback. // Optionally keep this method if you want to provide a manual port-incrementation fallback.
@@ -72,29 +67,23 @@ public class SPdfApplication {
} }
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
Properties props = new Properties(); Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false"); System.setProperty("java.awt.headless", "false");
app.setHeadless(false); app.setHeadless(false);
props.put("java.awt.headless", "false"); props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet"); props.put("spring.main.web-application-type", "servlet");
} }
app.setAdditionalProfiles("default"); app.setAdditionalProfiles("default");
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
// External config files // External config files
if (Files.exists(Paths.get("configs/settings.yml"))) { if (Files.exists(Paths.get("configs/settings.yml"))) {
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml"); propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
} else { } else {
log.warn("External configuration file 'configs/settings.yml' does not exist."); log.warn("External configuration file 'configs/settings.yml' does not exist.");
} }
if (Files.exists(Paths.get("configs/custom_settings.yml"))) { if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
String existingLocation = String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", ""); propertyFiles.getOrDefault("spring.config.additional-location", "");
@@ -108,21 +97,17 @@ public class SPdfApplication {
log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); log.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
} }
Properties finalProps = new Properties(); Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) { if (!propertyFiles.isEmpty()) {
finalProps.putAll( finalProps.putAll(
Collections.singletonMap( Collections.singletonMap(
"spring.config.additional-location", "spring.config.additional-location",
propertyFiles.get("spring.config.additional-location"))); propertyFiles.get("spring.config.additional-location")));
} }
if (!props.isEmpty()) { if (!props.isEmpty()) {
finalProps.putAll(props); finalProps.putAll(props);
} }
app.setDefaultProperties(finalProps); app.setDefaultProperties(finalProps);
app.run(args); app.run(args);
// Ensure directories are created // Ensure directories are created
try { try {
Files.createDirectories(Path.of("customFiles/static/")); Files.createDirectories(Path.of("customFiles/static/"));
@@ -130,7 +115,6 @@ public class SPdfApplication {
} catch (Exception e) { } catch (Exception e) {
log.error("Error creating directories: {}", e.getMessage()); log.error("Error creating directories: {}", e.getMessage());
} }
printStartupLogs(); printStartupLogs();
} }
@@ -140,8 +124,24 @@ public class SPdfApplication {
log.info("Navigate to {}", url); log.info("Navigate to {}", url);
} }
@Autowired(required = false) public static String getStaticBaseUrl() {
private WebBrowser webBrowser; return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic = // This will let Spring Boot assign an available port
"0";
} else {
SPdfApplication.serverPortStatic = port;
}
}
@PostConstruct @PostConstruct
public void init() { public void init() {
@@ -180,18 +180,10 @@ public class SPdfApplication {
} }
} }
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() { public String getNonStaticBaseUrl() {
return baseUrlStatic; return baseUrlStatic;
} }
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() { public String getNonStaticPort() {
return serverPortStatic; return serverPortStatic;
} }

View File

@@ -7,7 +7,6 @@ import java.nio.file.Paths;
import java.util.Properties; import java.util.Properties;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -27,7 +26,11 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class AppConfig { public class AppConfig {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public AppConfig(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
@@ -99,6 +102,25 @@ public class AppConfig {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Paths.get("/.dockerenv"));
} }
@Bean(name = "configDirMounted")
public boolean isRunningInDockerWithConfig() {
Path dockerEnv = Paths.get("/.dockerenv");
// default to true if not docker
if (!Files.exists(dockerEnv)) {
return true;
}
Path mountInfo = Paths.get("/proc/1/mountinfo");
// this should always exist, if not some unknown usecase
if (!Files.exists(mountInfo)) {
return true;
}
try {
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
} catch (IOException e) {
return false;
}
}
@Bean(name = "bookAndHtmlFormatsInstalled") @Bean(name = "bookAndHtmlFormatsInstalled")
public boolean bookAndHtmlFormatsInstalled() { public boolean bookAndHtmlFormatsInstalled() {
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS"); String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");

View File

@@ -11,10 +11,16 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
class AppUpdateService { class AppUpdateService {
@Autowired private ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired(required = false) private final ShowAdminInterface showAdmin;
ShowAdminInterface showAdmin;
public AppUpdateService(
ApplicationProperties applicationProperties,
@Autowired(required = false) ShowAdminInterface showAdmin) {
this.applicationProperties = applicationProperties;
this.showAdmin = showAdmin;
}
@Bean(name = "shouldShow") @Bean(name = "shouldShow")
@Scope("request") @Scope("request")

View File

@@ -20,11 +20,10 @@ import stirling.software.SPDF.model.ApplicationProperties;
@DependsOn({"bookAndHtmlFormatsInstalled"}) @DependsOn({"bookAndHtmlFormatsInstalled"})
public class EndpointConfiguration { public class EndpointConfiguration {
private static final String REMOVE_BLANKS = "remove-blanks";
private final ApplicationProperties applicationProperties;
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<>();
private final ApplicationProperties applicationProperties;
private boolean bookAndHtmlFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@Autowired @Autowired
@@ -287,6 +286,4 @@ public class EndpointConfiguration {
public Set<String> getEndpointsForGroup(String group) { public Set<String> getEndpointsForGroup(String group) {
return endpointGroups.getOrDefault(group, new HashSet<>()); return endpointGroups.getOrDefault(group, new HashSet<>());
} }
private static final String REMOVE_BLANKS = "remove-blanks";
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
@@ -10,7 +9,11 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
@Autowired private EndpointConfiguration endpointConfiguration; private final EndpointConfiguration endpointConfiguration;
public EndpointInterceptor(EndpointConfiguration endpointConfiguration) {
this.endpointConfiguration = endpointConfiguration;
}
@Override @Override
public boolean preHandle( public boolean preHandle(

View File

@@ -6,7 +6,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -15,7 +14,24 @@ import lombok.extern.slf4j.Slf4j;
@Configuration @Configuration
@Slf4j @Slf4j
public class ExternalAppDepConfig { public class ExternalAppDepConfig {
@Autowired private EndpointConfiguration endpointConfiguration;
private final EndpointConfiguration endpointConfiguration;
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("soffice", List.of("LibreOffice"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
}
};
public ExternalAppDepConfig(EndpointConfiguration endpointConfiguration) {
this.endpointConfiguration = endpointConfiguration;
}
private boolean isCommandAvailable(String command) { private boolean isCommandAvailable(String command) {
try { try {
@@ -34,18 +50,6 @@ public class ExternalAppDepConfig {
} }
} }
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("soffice", List.of("LibreOffice"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
}
};
private List<String> getAffectedFeatures(String group) { private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream() return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint)) .map(endpoint -> formatEndpointAsFeature(endpoint))
@@ -55,7 +59,6 @@ public class ExternalAppDepConfig {
private String formatEndpointAsFeature(String endpoint) { private String formatEndpointAsFeature(String endpoint) {
// First replace common terms // First replace common terms
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
// Split into words and capitalize each word // Split into words and capitalize each word
return Arrays.stream(feature.split("\\s+")) return Arrays.stream(feature.split("\\s+"))
.map(word -> capitalizeWord(word)) .map(word -> capitalizeWord(word))
@@ -76,7 +79,6 @@ public class ExternalAppDepConfig {
boolean isAvailable = isCommandAvailable(command); boolean isAvailable = isCommandAvailable(command);
if (!isAvailable) { if (!isAvailable) {
List<String> affectedGroups = commandToGroupMapping.get(command); List<String> affectedGroups = commandToGroupMapping.get(command);
if (affectedGroups != null) { if (affectedGroups != null) {
for (String group : affectedGroups) { for (String group : affectedGroups) {
List<String> affectedFeatures = getAffectedFeatures(group); List<String> affectedFeatures = getAffectedFeatures(group);
@@ -95,7 +97,6 @@ public class ExternalAppDepConfig {
@PostConstruct @PostConstruct
public void checkDependencies() { public void checkDependencies() {
// Check core dependencies // Check core dependencies
checkDependencyAndDisableGroup("tesseract"); checkDependencyAndDisableGroup("tesseract");
checkDependencyAndDisableGroup("soffice"); checkDependencyAndDisableGroup("soffice");
@@ -103,13 +104,11 @@ public class ExternalAppDepConfig {
checkDependencyAndDisableGroup("weasyprint"); checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml"); checkDependencyAndDisableGroup("pdftohtml");
checkDependencyAndDisableGroup("unoconv"); checkDependencyAndDisableGroup("unoconv");
// Special handling for Python/OpenCV dependencies // Special handling for Python/OpenCV dependencies
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python"); boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
if (!pythonAvailable) { if (!pythonAvailable) {
List<String> pythonFeatures = getAffectedFeatures("Python"); List<String> pythonFeatures = getAffectedFeatures("Python");
List<String> openCVFeatures = getAffectedFeatures("OpenCV"); List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("Python"); endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV"); endpointConfiguration.disableGroup("OpenCV");
log.warn( log.warn(

View File

@@ -4,7 +4,6 @@ import java.io.IOException;
import java.util.Properties; import java.util.Properties;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
@@ -23,25 +22,26 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Order(Ordered.HIGHEST_PRECEDENCE + 1) @Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class InitialSetup { public class InitialSetup {
@Autowired private ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public InitialSetup(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@PostConstruct @PostConstruct
public void init() throws IOException { public void init() throws IOException {
initUUIDKey(); initUUIDKey();
initSecretKey(); initSecretKey();
initEnableCSRFSecurity(); initEnableCSRFSecurity();
initLegalUrls(); initLegalUrls();
initSetAppVersion(); initSetAppVersion();
} }
public void initUUIDKey() throws IOException { public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) { if (!GeneralUtils.isValidUUID(uuid)) {
uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key // Generating a random UUID as the secret key
uuid = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
applicationProperties.getAutomaticallyGenerated().setUUID(uuid); applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
} }
@@ -50,7 +50,8 @@ public class InitialSetup {
public void initSecretKey() throws IOException { public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) { if (!GeneralUtils.isValidUUID(secretKey)) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key // Generating a random UUID as the secret key
secretKey = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
applicationProperties.getAutomaticallyGenerated().setKey(secretKey); applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
} }
@@ -76,7 +77,6 @@ public class InitialSetup {
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
} }
// Initialize Privacy Policy // Initialize Privacy Policy
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) { if (StringUtils.isEmpty(privacyUrl)) {
@@ -87,7 +87,6 @@ public class InitialSetup {
} }
public void initSetAppVersion() throws IOException { public void initSetAppVersion() throws IOException {
String appVersion = "0.0.0"; String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties"); Resource resource = new ClassPathResource("version.properties");
Properties props = new Properties(); Properties props = new Properties();
@@ -95,7 +94,6 @@ public class InitialSetup {
props.load(resource.getInputStream()); props.load(resource.getInputStream());
appVersion = props.getProperty("version"); appVersion = props.getProperty("version");
} catch (Exception e) { } catch (Exception e) {
} }
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion); applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF.config;
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;
@@ -16,7 +15,11 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class LocaleConfiguration implements WebMvcConfigurer { public class LocaleConfiguration implements WebMvcConfigurer {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public LocaleConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@@ -34,21 +37,17 @@ public class LocaleConfiguration implements WebMvcConfigurer {
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale defaultLocale = // Fallback to UK locale if environment variable is not set
Locale.UK; // Fallback to UK locale if environment variable is not set Locale.UK;
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
@@ -57,7 +56,6 @@ public class LocaleConfiguration implements WebMvcConfigurer {
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
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;
@@ -15,15 +14,19 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public OpenApiConfig(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
version = "1.0.0"; // default version if all else fails // default version if all else fails
version = "1.0.0";
} }
SecurityScheme apiKeyScheme = SecurityScheme apiKeyScheme =
new SecurityScheme() new SecurityScheme()
.type(SecurityScheme.Type.APIKEY) .type(SecurityScheme.Type.APIKEY)

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -9,7 +8,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired private EndpointInterceptor endpointInterceptor; private final EndpointInterceptor endpointInterceptor;
public WebMvcConfig(EndpointInterceptor endpointInterceptor) {
this.endpointInterceptor = endpointInterceptor;
}
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {

View File

@@ -6,6 +6,7 @@ import java.util.List;
import stirling.software.SPDF.utils.FileInfo; import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseBackupInterface { public interface DatabaseBackupInterface {
void exportDatabase() throws IOException; void exportDatabase() throws IOException;
boolean importDatabase(); boolean importDatabase();

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF.config.security;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,8 +14,15 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
class AppUpdateAuthService implements ShowAdminInterface { class AppUpdateAuthService implements ShowAdminInterface {
@Autowired private UserRepository userRepository; private final UserRepository userRepository;
@Autowired private ApplicationProperties applicationProperties;
private final ApplicationProperties applicationProperties;
public AppUpdateAuthService(
UserRepository userRepository, ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.applicationProperties = applicationProperties;
}
@Override @Override
public boolean getShowUpdateOnlyAdmins() { public boolean getShowUpdateOnlyAdmins() {
@@ -24,24 +30,18 @@ class AppUpdateAuthService implements ShowAdminInterface {
if (!showUpdate) { if (!showUpdate) {
return showUpdate; return showUpdate;
} }
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin(); boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
return !showUpdateOnlyAdmin; return !showUpdateOnlyAdmin;
} }
if (authentication.getName().equalsIgnoreCase("anonymousUser")) { if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
return !showUpdateOnlyAdmin; return !showUpdateOnlyAdmin;
} }
Optional<User> user = userRepository.findByUsername(authentication.getName()); Optional<User> user = userRepository.findByUsername(authentication.getName());
if (user.isPresent() && showUpdateOnlyAdmin) { if (user.isPresent() && showUpdateOnlyAdmin) {
return "ROLE_ADMIN".equals(user.get().getRolesAsString()); return "ROLE_ADMIN".equals(user.get().getRolesAsString());
} }
return showUpdate; return showUpdate;
} }
} }

View File

@@ -219,9 +219,9 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL"); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl); // log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
// break; // break;
default: default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param; String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl); log.info("Redirecting to default logout URL: " + defaultRedirectUrl);

View File

@@ -4,7 +4,6 @@ import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -20,9 +19,15 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@Autowired private UserRepository userRepository; private final UserRepository userRepository;
@Autowired private LoginAttemptService loginAttemptService; private final LoginAttemptService loginAttemptService;
public CustomUserDetailsService(
UserRepository userRepository, LoginAttemptService loginAttemptService) {
this.userRepository = userRepository;
this.loginAttemptService = loginAttemptService;
}
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@@ -33,16 +38,13 @@ public class CustomUserDetailsService implements UserDetailsService {
() -> () ->
new UsernameNotFoundException( new UsernameNotFoundException(
"No user found with username: " + username)); "No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (!user.hasPassword()) { if (!user.hasPassword()) {
throw new IllegalArgumentException("Password must not be null"); throw new IllegalArgumentException("Password must not be null");
} }
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),

View File

@@ -5,7 +5,6 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@@ -25,7 +24,11 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@Autowired @Lazy private UserService userService; @Lazy private final UserService userService;
public FirstLoginFilter(@Lazy UserService userService) {
this.userService = userService;
}
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@@ -34,16 +37,13 @@ public class FirstLoginFilter extends OncePerRequestFilter {
String method = request.getMethod(); String method = request.getMethod();
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
// Check if the request is for static resources // Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI); boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) { if (isStaticResource) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName()); Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName());
@@ -55,12 +55,10 @@ public class FirstLoginFilter extends OncePerRequestFilter {
return; return;
} }
} }
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
HttpSession session = request.getSession(true); HttpSession session = request.getSession(true);
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
String creationTime = timeFormat.format(new Date(session.getCreationTime())); String creationTime = timeFormat.format(new Date(session.getCreationTime()));
log.debug( log.debug(
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}", "Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
session.isNew(), session.isNew(),

View File

@@ -4,11 +4,7 @@ import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.Filter; import jakarta.servlet.*;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;

View File

@@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -16,11 +15,20 @@ import stirling.software.SPDF.model.Role;
@Slf4j @Slf4j
public class InitialSecuritySetup { public class InitialSecuritySetup {
@Autowired private UserService userService; private final UserService userService;
@Autowired private ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired private DatabaseBackupInterface databaseBackupHelper; private final DatabaseBackupInterface databaseBackupHelper;
public InitialSecuritySetup(
UserService userService,
ApplicationProperties applicationProperties,
DatabaseBackupInterface databaseBackupHelper) {
this.userService = userService;
this.applicationProperties = applicationProperties;
this.databaseBackupHelper = databaseBackupHelper;
}
@PostConstruct @PostConstruct
public void init() throws IllegalArgumentException, IOException { public void init() throws IllegalArgumentException, IOException {

View File

@@ -3,7 +3,6 @@ package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -15,13 +14,20 @@ import stirling.software.SPDF.model.AttemptCounter;
@Slf4j @Slf4j
public class LoginAttemptService { public class LoginAttemptService {
@Autowired private ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private int MAX_ATTEMPT; private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache; private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
private boolean isBlockedEnabled = true; private boolean isBlockedEnabled = true;
public LoginAttemptService(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
@@ -46,7 +52,6 @@ public class LoginAttemptService {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return; return;
} }
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
attemptCounter = new AttemptCounter(); attemptCounter = new AttemptCounter();
@@ -67,20 +72,18 @@ public class LoginAttemptService {
if (attemptCounter == null) { if (attemptCounter == null) {
return false; return false;
} }
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT; return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
} }
public int getRemainingAttempts(String key) { public int getRemainingAttempts(String key) {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled // Arbitrarily high number if tracking is disabled
return Integer.MAX_VALUE;
} }
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
return MAX_ATTEMPT; return MAX_ATTEMPT;
} }
return MAX_ATTEMPT - attemptCounter.getAttemptCount(); return MAX_ATTEMPT - attemptCounter.getAttemptCount();
} }
} }

View File

@@ -4,7 +4,6 @@ import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest; import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -63,6 +62,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
import stirling.software.SPDF.repository.PersistentLoginRepository;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -71,38 +71,64 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@DependsOn("runningEE") @DependsOn("runningEE")
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; private final CustomUserDetailsService userDetailsService;
@Lazy private final UserService userService;
@Qualifier("loginEnabled")
private final boolean loginEnabledValue;
@Qualifier("runningEE")
private final boolean runningEE;
private final ApplicationProperties applicationProperties;
private final UserAuthenticationFilter userAuthenticationFilter;
private final LoginAttemptService loginAttemptService;
private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry;
private final PersistentLoginRepository persistentLoginRepository;
// // Only Dev test
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) ->
// web.ignoring()
// .requestMatchers(
// "/css/**", "/images/**", "/js/**", "/**.svg",
// "/pdfjs-legacy/**");
// }
public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository,
CustomUserDetailsService userDetailsService,
@Lazy UserService userService,
@Qualifier("loginEnabled") boolean loginEnabledValue,
@Qualifier("runningEE") boolean runningEE,
ApplicationProperties applicationProperties,
UserAuthenticationFilter userAuthenticationFilter,
LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry) {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
this.runningEE = runningEE;
this.applicationProperties = applicationProperties;
this.userAuthenticationFilter = userAuthenticationFilter;
this.loginAttemptService = loginAttemptService;
this.firstLoginFilter = firstLoginFilter;
this.sessionRegistry = sessionRegistry;
this.persistentLoginRepository = persistentLoginRepository;
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Autowired @Lazy private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
} }
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -117,13 +143,11 @@ public class SecurityConfiguration {
csrf.ignoringRequestMatchers( csrf.ignoringRequestMatchers(
request -> { request -> {
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF // If there's no API key, don't ignore CSRF
// (return false) // (return false)
if (apiKey == null || apiKey.trim().isEmpty()) { if (apiKey == null || apiKey.trim().isEmpty()) {
return false; return false;
} }
// Validate API key using existing UserService // Validate API key using existing UserService
try { try {
Optional<User> user = Optional<User> user =
@@ -152,7 +176,6 @@ public class SecurityConfiguration {
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
http.authenticationProvider(daoAuthenticationProvider()); http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
http.logout( http.logout(
@@ -161,18 +184,23 @@ public class SecurityConfiguration {
.logoutSuccessHandler( .logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties)) new CustomLogoutSuccessHandler(applicationProperties))
.clearAuthentication(true) .clearAuthentication(true)
.invalidateHttpSession(true) // Invalidate session .invalidateHttpSession( // Invalidate session
true)
.deleteCookies("JSESSIONID", "remember-me")); .deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe( http.rememberMe(
rememberMeConfigurer -> rememberMeConfigurer -> // Use the configurator directly
rememberMeConfigurer // Use the configurator directly rememberMeConfigurer
.tokenRepository(persistentTokenRepository()) .tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days .tokenValiditySeconds( // 14 days
.userDetailsService( 14 * 24 * 60 * 60)
userDetailsService) // Your existing UserDetailsService .userDetailsService( // Your existing UserDetailsService
.useSecureCookie(true) // Enable secure cookie userDetailsService)
.rememberMeParameter("remember-me") // Form parameter name .useSecureCookie( // Enable secure cookie
.rememberMeCookieName("remember-me") // Cookie name true)
.rememberMeParameter( // Form parameter name
"remember-me")
.rememberMeCookieName( // Cookie name
"remember-me")
.alwaysRemember(false)); .alwaysRemember(false));
http.authorizeHttpRequests( http.authorizeHttpRequests(
authz -> authz ->
@@ -180,14 +208,12 @@ public class SecurityConfiguration {
req -> { req -> {
String uri = req.getRequestURI(); String uri = req.getRequestURI();
String contextPath = req.getContextPath(); String contextPath = req.getContextPath();
// Remove the context path from the URI // Remove the context path from the URI
String trimmedUri = String trimmedUri =
uri.startsWith(contextPath) uri.startsWith(contextPath)
? uri.substring( ? uri.substring(
contextPath.length()) contextPath.length())
: uri; : uri;
return trimmedUri.startsWith("/login") return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2") || trimmedUri.startsWith("/saml2")
@@ -205,7 +231,6 @@ public class SecurityConfiguration {
.permitAll() .permitAll()
.anyRequest() .anyRequest()
.authenticated()); .authenticated());
// Handle User/Password Logins // Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) { if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin( http.formLogin(
@@ -221,27 +246,26 @@ public class SecurityConfiguration {
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.permitAll()); .permitAll());
} }
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().isOauth2Activ()) { if (applicationProperties.getSecurity().isOauth2Activ()) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
oauth2.loginPage("/oauth2") oauth2.loginPage("/oauth2")
.
/* /*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser' If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same. is set as true, else login fails with an error message advising the same.
*/ */
.successHandler( successHandler(
new CustomOAuth2AuthenticationSuccessHandler( new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
applicationProperties, applicationProperties,
userService)) userService))
.failureHandler( .failureHandler(
new CustomOAuth2AuthenticationFailureHandler()) new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database . // Add existing Authorities from the database
.userInfoEndpoint( userInfoEndpoint(
userInfoEndpoint -> userInfoEndpoint ->
userInfoEndpoint userInfoEndpoint
.oidcUserService( .oidcUserService(
@@ -253,15 +277,14 @@ public class SecurityConfiguration {
userAuthoritiesMapper())) userAuthoritiesMapper()))
.permitAll()); .permitAll());
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE if (applicationProperties.getSecurity().isSaml2Activ()) {
// && runningEE
// Configure the authentication provider // Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter( authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService)); new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider) http.authenticationProvider(authenticationProvider)
.saml2Login( .saml2Login(
saml2 -> { saml2 -> {
@@ -287,7 +310,6 @@ public class SecurityConfiguration {
} }
}); });
} }
} else { } else {
// if (!applicationProperties.getSecurity().getCsrfDisabled()) { // if (!applicationProperties.getSecurity().getCsrfDisabled()) {
// CookieCsrfTokenRepository cookieRepo = // CookieCsrfTokenRepository cookieRepo =
@@ -302,7 +324,6 @@ public class SecurityConfiguration {
// } // }
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
return http.build(); return http.build();
} }
@@ -313,17 +334,14 @@ public class SecurityConfiguration {
matchIfMissing = false) matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() { public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>(); List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add); githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add); oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add); googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add); keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) { if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured"); log.error("At least one OAuth2 provider must be configured");
System.exit(1); System.exit(1);
} }
return new InMemoryClientRegistrationRepository(registrations); return new InMemoryClientRegistrationRepository(registrations);
} }
@@ -366,7 +384,6 @@ public class SecurityConfiguration {
return Optional.empty(); return Optional.empty();
} }
KeycloakProvider keycloak = client.getKeycloak(); KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid() return keycloak != null && keycloak.isSettingsValid()
? Optional.of( ? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer()) ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
@@ -381,7 +398,6 @@ public class SecurityConfiguration {
} }
private Optional<ClientRegistration> githubClientRegistration() { private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2(); OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) { if (oauth == null || !oauth.getEnabled()) {
return Optional.empty(); return Optional.empty();
@@ -443,19 +459,15 @@ public class SecurityConfiguration {
matchIfMissing = false) matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert()); X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert); Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey(); Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert(); Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential = Saml2X509Credential signingCredential =
new Saml2X509Credential( new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource), CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource), CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING); Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp = RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential)) .signingX509Credentials(c -> c.add(signingCredential))
@@ -470,7 +482,6 @@ public class SecurityConfiguration {
Saml2MessageBinding.POST) Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true)) .wantAuthnRequestsSigned(true))
.build(); .build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
} }
@@ -486,10 +497,8 @@ public class SecurityConfiguration {
resolver.setAuthnRequestCustomizer( resolver.setAuthnRequestCustomizer(
customizer -> { customizer -> {
log.debug("Customizing SAML Authentication request"); log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest(); AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID()); log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) { if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString()); authnRequest.setID("ARQ" + UUID.randomUUID().toString());
} }
@@ -500,16 +509,13 @@ public class SecurityConfiguration {
authnRequest.getIssuer() != null authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue() ? authnRequest.getIssuer().getValue()
: "null"); : "null");
HttpServletRequest request = customizer.getRequest(); HttpServletRequest request = customizer.getRequest();
// Log HTTP request details // Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod()); log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI()); log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString()); log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString()); log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr()); log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers // Log headers
Collections.list(request.getHeaderNames()) Collections.list(request.getHeaderNames())
.forEach( .forEach(
@@ -519,24 +525,20 @@ public class SecurityConfiguration {
headerName, headerName,
request.getHeader(headerName)); request.getHeader(headerName));
}); });
// Log SAML specific parameters // Log SAML specific parameters
log.debug("SAML Request Parameters:"); log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest")); log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState")); log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists // Log session debugrmation if exists
if (request.getSession(false) != null) { if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId()); log.debug("Session ID: {}", request.getSession().getId());
} }
// Log any assertions consumer service details if present // Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) { if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug( log.debug(
"AssertionConsumerServiceURL: {}", "AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL()); authnRequest.getAssertionConsumerServiceURL());
} }
// Log NameID policy if present // Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) { if (authnRequest.getNameIDPolicy() != null) {
log.debug( log.debug(
@@ -566,12 +568,10 @@ public class SecurityConfiguration {
GrantedAuthoritiesMapper userAuthoritiesMapper() { GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> { return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>(); Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach( authorities.forEach(
authority -> { authority -> {
// Add existing OAUTH2 Authorities // Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority())); mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present. // Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) { if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername = String useAsUsername =
@@ -598,27 +598,18 @@ public class SecurityConfiguration {
@Bean @Bean
public IPRateLimitingFilter rateLimitingFilter() { public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level // Example limit TODO add config level
int maxRequestsPerIp = 1000000;
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
} }
@Bean @Bean
public PersistentTokenRepository persistentTokenRepository() { public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(); return new JPATokenRepositoryImpl(persistentLoginRepository);
} }
@Bean @Bean
public boolean activSecurity() { public boolean activSecurity() {
return true; return true;
} }
// // Only Dev test
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) ->
// web.ignoring()
// .requestMatchers(
// "/css/**", "/images/**", "/js/**", "/**.svg",
// "/pdfjs-legacy/**");
// }
} }

View File

@@ -5,14 +5,12 @@ import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -31,13 +29,15 @@ import stirling.software.SPDF.model.Role;
public class UserBasedRateLimitingFilter extends OncePerRequestFilter { public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired private UserDetailsService userDetailsService;
@Autowired
@Qualifier("rateLimit") @Qualifier("rateLimit")
public boolean rateLimit; private final boolean rateLimit;
public UserBasedRateLimitingFilter(@Qualifier("rateLimit") boolean rateLimit) {
this.rateLimit = rateLimit;
}
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@@ -48,21 +48,18 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String method = request.getMethod(); String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) { if (!"POST".equalsIgnoreCase(method)) {
// If the request is not a POST, just pass it through without rate limiting // If the request is not a POST, just pass it through without rate limiting
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String identifier = null; String identifier = null;
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-KEY");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = identifier = // Prefix to distinguish between API keys and usernames
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames "API_KEY_" + apiKey;
} else { } else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
@@ -70,15 +67,12 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
identifier = userDetails.getUsername(); identifier = userDetails.getUsername();
} }
} }
// If neither API key nor an authenticated user is present, use IP address // If neither API key nor an authenticated user is present, use IP address
if (identifier == null) { if (identifier == null) {
identifier = request.getRemoteAddr(); identifier = request.getRemoteAddr();
} }
Role userRole = Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-KEY") != null) { if (request.getHeader("X-API-KEY") != null) {
// It's an API call // It's an API call
processRequest( processRequest(
@@ -123,7 +117,6 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
throws IOException, ServletException { throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) { if (probe.isConsumed()) {
response.setHeader( response.setHeader(
"X-Rate-Limit-Remaining", "X-Rate-Limit-Remaining",

View File

@@ -4,7 +4,6 @@ import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -25,11 +24,7 @@ import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@@ -37,19 +32,36 @@ import stirling.software.SPDF.repository.UserRepository;
@Slf4j @Slf4j
public class UserService implements UserServiceInterface { public class UserService implements UserServiceInterface {
@Autowired private UserRepository userRepository; private final UserRepository userRepository;
@Autowired private AuthorityRepository authorityRepository; private final AuthorityRepository authorityRepository;
@Autowired private PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
@Autowired private MessageSource messageSource; private final MessageSource messageSource;
@Autowired private SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
@Autowired DatabaseBackupInterface databaseBackupHelper; private final DatabaseBackupInterface databaseBackupHelper;
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public UserService(
UserRepository userRepository,
AuthorityRepository authorityRepository,
PasswordEncoder passwordEncoder,
MessageSource messageSource,
SessionPersistentRegistry sessionRegistry,
DatabaseBackupInterface databaseBackupHelper,
ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.authorityRepository = authorityRepository;
this.passwordEncoder = passwordEncoder;
this.messageSource = messageSource;
this.sessionRegistry = sessionRegistry;
this.databaseBackupHelper = databaseBackupHelper;
this.applicationProperties = applicationProperties;
}
@Transactional @Transactional
public void migrateOauth2ToSSO() { public void migrateOauth2ToSSO() {
@@ -84,13 +96,11 @@ public class UserService implements UserServiceInterface {
if (!user.isPresent()) { if (!user.isPresent()) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
// Convert the user into an Authentication object // Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken( return new UsernamePasswordAuthenticationToken( // principal (typically the user)
user, // principal (typically the user) user, // credentials (we don't expose the password or API key here)
null, // credentials (we don't expose the password or API key here) null, // user's authorities (roles/permissions)
getAuthorities(user.get()) // user's authorities (roles/permissions) getAuthorities(user.get()));
);
} }
private Collection<? extends GrantedAuthority> getAuthorities(User user) { private Collection<? extends GrantedAuthority> getAuthorities(User user) {
@@ -104,7 +114,8 @@ public class UserService implements UserServiceInterface {
String apiKey; String apiKey;
do { do {
apiKey = UUID.randomUUID().toString(); apiKey = UUID.randomUUID().toString();
} while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness } while ( // Ensure uniqueness
userRepository.findByApiKey(apiKey).isPresent());
return apiKey; return apiKey;
} }
@@ -118,7 +129,8 @@ public class UserService implements UserServiceInterface {
} }
public User refreshApiKeyForUser(String username) { public User refreshApiKeyForUser(String username) {
return addApiKeyToUser(username); // reuse the add API key method for refreshing // reuse the add API key method for refreshing
return addApiKeyToUser(username);
} }
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
@@ -138,11 +150,11 @@ public class UserService implements UserServiceInterface {
public Optional<User> loadUserByApiKey(String apiKey) { public Optional<User> loadUserByApiKey(String apiKey) {
Optional<User> user = userRepository.findByApiKey(apiKey); Optional<User> user = userRepository.findByApiKey(apiKey);
if (user.isPresent()) { if (user.isPresent()) {
return user; return user;
} }
return null; // or throw an exception // or throw an exception
return null;
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
@@ -240,14 +252,12 @@ public class UserService implements UserServiceInterface {
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
if (settingsMap == null) { if (settingsMap == null) {
settingsMap = new HashMap<>(); settingsMap = new HashMap<>();
} }
settingsMap.clear(); settingsMap.clear();
settingsMap.putAll(updates); settingsMap.putAll(updates);
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
@@ -316,12 +326,9 @@ public class UserService implements UserServiceInterface {
boolean isValidEmail = boolean isValidEmail =
username.matches( username.matches(
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$"); "^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
List<String> notAllowedUserList = new ArrayList<>(); List<String> notAllowedUserList = new ArrayList<>();
notAllowedUserList.add("ALL_USERS".toLowerCase()); notAllowedUserList.add("ALL_USERS".toLowerCase());
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase()); boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser; return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
} }
@@ -374,7 +381,6 @@ public class UserService implements UserServiceInterface {
public String getCurrentUsername() { public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername(); return ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
@@ -397,7 +403,6 @@ public class UserService implements UserServiceInterface {
} }
String username = "CUSTOM_API_USER"; String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username); Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) { if (!existingUser.isPresent()) {
// Create new user with API role // Create new user with API role
User user = new User(); User user = new User();

View File

@@ -6,12 +6,7 @@ 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.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection; import java.sql.*;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;

View File

@@ -2,14 +2,17 @@ package stirling.software.SPDF.config.security.database;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class ScheduledTasks { public class ScheduledTasks {
@Autowired private DatabaseBackupHelper databaseBackupService; private final DatabaseBackupHelper databaseBackupService;
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
this.databaseBackupService = databaseBackupService;
}
@Scheduled(cron = "0 0 0 * * ?") @Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws IOException { public void performBackup() throws IOException {

View File

@@ -1,12 +1,7 @@
package stirling.software.SPDF.config.security.session; package stirling.software.SPDF.config.security.session;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;

View File

@@ -5,19 +5,22 @@ import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class SessionScheduled { public class SessionScheduled {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
private final SessionPersistentRegistry sessionPersistentRegistry;
public SessionScheduled(SessionPersistentRegistry sessionPersistentRegistry) {
this.sessionPersistentRegistry = sessionPersistentRegistry;
}
@Scheduled(cron = "0 0/5 * * * ?") @Scheduled(cron = "0 0/5 * * * ?")
public void expireSessions() { public void expireSessions() {
Instant now = Instant.now(); Instant now = Instant.now();
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) { for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
List<SessionInformation> sessionInformations = List<SessionInformation> sessionInformations =
sessionPersistentRegistry.getAllSessions(principal, false); sessionPersistentRegistry.getAllSessions(principal, false);

View File

@@ -4,7 +4,6 @@ import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@@ -18,35 +17,35 @@ import stirling.software.SPDF.service.LanguageService;
@RequestMapping("/js") @RequestMapping("/js")
public class AdditionalLanguageJsController { public class AdditionalLanguageJsController {
@Autowired private LanguageService languageService; private final LanguageService languageService;
public AdditionalLanguageJsController(LanguageService languageService) {
this.languageService = languageService;
}
@Hidden @Hidden
@GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript") @GetMapping(value = "/additionalLanguageCode.js", produces = "application/javascript")
public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException { public void generateAdditionalLanguageJs(HttpServletResponse response) throws IOException {
List<String> supportedLanguages = languageService.getSupportedLanguages(); List<String> supportedLanguages = languageService.getSupportedLanguages();
response.setContentType("application/javascript"); response.setContentType("application/javascript");
PrintWriter writer = response.getWriter(); PrintWriter writer = response.getWriter();
// Erstelle das JavaScript dynamisch // Erstelle das JavaScript dynamisch
writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";"); writer.println("const supportedLanguages = " + toJsonArray(supportedLanguages) + ";");
// Generiere die `getDetailedLanguageCode`-Funktion // Generiere die `getDetailedLanguageCode`-Funktion
writer.println( writer.println(
""" """
function getDetailedLanguageCode() { function getDetailedLanguageCode() {
const userLanguages = navigator.languages ? navigator.languages : [navigator.language]; const userLanguages = navigator.languages ? navigator.languages : [navigator.language];
for (let lang of userLanguages) { for (let lang of userLanguages) {
let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_'))); let matchedLang = supportedLanguages.find(supportedLang => supportedLang.startsWith(lang.replace('-', '_')));
if (matchedLang) { if (matchedLang) {
return matchedLang; return matchedLang;
}
}
// Fallback
return "en_GB";
} }
} """);
// Fallback
return "en_GB";
}
""");
writer.flush(); writer.flush();
} }

View File

@@ -8,23 +8,19 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -34,19 +30,25 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Controller @Controller
@RequestMapping("/api/v1/database") @RequestMapping("/api/v1/database")
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@Tag(name = "Database", description = "Database APIs") @Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController { public class DatabaseController {
@Autowired DatabaseBackupHelper databaseBackupHelper; private final DatabaseBackupHelper databaseBackupHelper;
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseBackupHelper = databaseBackupHelper;
}
@Hidden
@PostMapping(consumes = "multipart/form-data", value = "import-database")
@Operation( @Operation(
summary = "Import database backup", summary = "Import a database backup file",
description = "This endpoint imports a database backup from a SQL file.") description = "Uploads and imports a database backup SQL file.")
@PostMapping(consumes = "multipart/form-data", value = "import-database")
public String importDatabase( public String importDatabase(
@RequestParam("fileInput") MultipartFile file, RedirectAttributes redirectAttributes) @Parameter(description = "SQL file to import", required = true)
throws IllegalArgumentException, IOException { @RequestParam("fileInput")
MultipartFile file,
RedirectAttributes redirectAttributes)
throws IOException {
if (file == null || file.isEmpty()) { if (file == null || file.isEmpty()) {
redirectAttributes.addAttribute("error", "fileNullOrEmpty"); redirectAttributes.addAttribute("error", "fileNullOrEmpty");
return "redirect:/database"; return "redirect:/database";
@@ -69,13 +71,17 @@ public class DatabaseController {
} }
@Hidden @Hidden
@Operation(
summary = "Import database backup by filename",
description = "Imports a database backup file from the server using its file name.")
@GetMapping("/import-database-file/{fileName}") @GetMapping("/import-database-file/{fileName}")
public String importDatabaseFromBackupUI(@PathVariable String fileName) public String importDatabaseFromBackupUI(
throws IllegalArgumentException, IOException { @Parameter(description = "Name of the file to import", required = true) @PathVariable
String fileName)
throws IOException {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty"; return "redirect:/database?error=fileNullOrEmpty";
} }
// Check if the file exists in the backup list // Check if the file exists in the backup list
boolean fileExists = boolean fileExists =
databaseBackupHelper.getBackupList().stream() databaseBackupHelper.getBackupList().stream()
@@ -93,12 +99,13 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/delete/{fileName}")
@Operation( @Operation(
summary = "Delete a database backup file", summary = "Delete a database backup file",
description = description = "Deletes a specified database backup file from the server.")
"This endpoint deletes a database backup file with the specified file name.") @GetMapping("/delete/{fileName}")
public String deleteFile(@PathVariable String fileName) { public String deleteFile(
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
@@ -117,12 +124,13 @@ public class DatabaseController {
} }
@Hidden @Hidden
@GetMapping("/download/{fileName}")
@Operation( @Operation(
summary = "Download a database backup file", summary = "Download a database backup file",
description = description = "Downloads the specified database backup file from the server.")
"This endpoint downloads a database backup file with the specified file name.") @GetMapping("/download/{fileName}")
public ResponseEntity<?> downloadFile(@PathVariable String fileName) { public ResponseEntity<?> downloadFile(
@Parameter(description = "Name of the file to download", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) { if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty"); throw new IllegalArgumentException("File must not be null or empty");
} }
@@ -141,4 +149,22 @@ public class DatabaseController {
.build(); .build();
} }
} }
@Operation(
summary = "Create a database backup",
description =
"This endpoint triggers the creation of a database backup and redirects to the"
+ " database management page.")
@GetMapping("/createDatabaseBackup")
public String createDatabaseBackup() {
try {
log.info("Starting database backup creation...");
databaseBackupHelper.exportDatabase();
log.info("Database backup successfully created.");
} catch (IOException e) {
log.error("Error creating database backup: {}", e.getMessage(), e);
return "redirect:/database?error=" + e.getMessage();
}
return "redirect:/database?infoMessage=backupCreated";
}
} }

View File

@@ -1,6 +1,6 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.awt.Color; import java.awt.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;

View File

@@ -6,7 +6,10 @@ import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;

View File

@@ -2,11 +2,12 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -20,7 +21,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Hidden @Hidden
public class SettingsController { public class SettingsController {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public SettingsController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@PostMapping("/update-enable-analytics") @PostMapping("/update-enable-analytics")
@Hidden @Hidden
@@ -32,7 +37,6 @@ public class SettingsController {
} }
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false); GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled)); applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
return ResponseEntity.ok("Updated"); return ResponseEntity.ok("Updated");
} }
} }

View File

@@ -49,6 +49,74 @@ public class SplitPdfByChaptersController {
this.pdfMetadataService = pdfMetadataService; this.pdfMetadataService = pdfMetadataService;
} }
private static List<Bookmark> extractOutlineItems(
PDDocument sourceDocument,
PDOutlineItem current,
List<Bookmark> bookmarks,
PDOutlineItem nextParent,
int level,
int maxLevel)
throws Exception {
while (current != null) {
String currentTitle = current.getTitle().replace("/", "");
int firstPage =
sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument));
PDOutlineItem child = current.getFirstChild();
PDOutlineItem nextSibling = current.getNextSibling();
int endPage;
if (child != null && level < maxLevel) {
endPage =
sourceDocument
.getPages()
.indexOf(child.findDestinationPage(sourceDocument));
} else if (nextSibling != null) {
endPage =
sourceDocument
.getPages()
.indexOf(nextSibling.findDestinationPage(sourceDocument));
} else if (nextParent != null) {
endPage =
sourceDocument
.getPages()
.indexOf(nextParent.findDestinationPage(sourceDocument));
} else {
endPage = -2;
/*
happens when we have something like this:
Outline Item 2
Outline Item 2.1
Outline Item 2.1.1
Outline Item 2.2
Outline 2.2.1
Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling
Outline Item 3
*/
}
if (!bookmarks.isEmpty()
&& bookmarks.get(bookmarks.size() - 1).getEndPage() == -2
&& firstPage
>= bookmarks
.get(bookmarks.size() - 1)
.getStartPage()) { // for handling the above-mentioned case
Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1);
previousBookmark.setEndPage(firstPage);
}
bookmarks.add(new Bookmark(currentTitle, firstPage, endPage));
// Recursively process children
if (child != null && level < maxLevel) {
extractOutlineItems(
sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel);
}
current = nextSibling;
}
return bookmarks;
}
@PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data") @PostMapping(value = "/split-pdf-by-chapters", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Split PDFs by Chapters", summary = "Split PDFs by Chapters",
@@ -163,74 +231,6 @@ public class SplitPdfByChaptersController {
return bookmarks; return bookmarks;
} }
private static List<Bookmark> extractOutlineItems(
PDDocument sourceDocument,
PDOutlineItem current,
List<Bookmark> bookmarks,
PDOutlineItem nextParent,
int level,
int maxLevel)
throws Exception {
while (current != null) {
String currentTitle = current.getTitle().replace("/", "");
int firstPage =
sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument));
PDOutlineItem child = current.getFirstChild();
PDOutlineItem nextSibling = current.getNextSibling();
int endPage;
if (child != null && level < maxLevel) {
endPage =
sourceDocument
.getPages()
.indexOf(child.findDestinationPage(sourceDocument));
} else if (nextSibling != null) {
endPage =
sourceDocument
.getPages()
.indexOf(nextSibling.findDestinationPage(sourceDocument));
} else if (nextParent != null) {
endPage =
sourceDocument
.getPages()
.indexOf(nextParent.findDestinationPage(sourceDocument));
} else {
endPage = -2;
/*
happens when we have something like this:
Outline Item 2
Outline Item 2.1
Outline Item 2.1.1
Outline Item 2.2
Outline 2.2.1
Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling
Outline Item 3
*/
}
if (!bookmarks.isEmpty()
&& bookmarks.get(bookmarks.size() - 1).getEndPage() == -2
&& firstPage
>= bookmarks
.get(bookmarks.size() - 1)
.getStartPage()) { // for handling the above-mentioned case
Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1);
previousBookmark.setEndPage(firstPage);
}
bookmarks.add(new Bookmark(currentTitle, firstPage, endPage));
// Recursively process children
if (child != null && level < maxLevel) {
extractOutlineItems(
sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel);
}
current = nextSibling;
}
return bookmarks;
}
private Path createZipFile( private Path createZipFile(
List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas) List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas)
throws Exception { throws Exception {

View File

@@ -7,7 +7,6 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -18,11 +17,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
@@ -45,9 +40,14 @@ import stirling.software.SPDF.model.api.user.UsernameAndPass;
@Slf4j @Slf4j
public class UserController { public class UserController {
@Autowired private UserService userService; private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
private final UserService userService;
private final SessionPersistentRegistry sessionRegistry;
@Autowired SessionPersistentRegistry sessionRegistry; public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
this.userService = userService;
this.sessionRegistry = sessionRegistry;
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
@@ -75,36 +75,27 @@ public class UserController {
HttpServletResponse response, HttpServletResponse response,
RedirectAttributes redirectAttributes) RedirectAttributes redirectAttributes)
throws IOException { throws IOException {
if (!userService.isUsernameValid(newUsername)) { if (!userService.isUsernameValid(newUsername)) {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
} }
if (principal == null) { if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
// The username MUST be unique when renaming // The username MUST be unique when renaming
Optional<User> userOpt = userService.findByUsername(principal.getName()); Optional<User> userOpt = userService.findByUsername(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound", true); return new RedirectView("/account?messageType=userNotFound", true);
} }
User user = userOpt.get(); User user = userOpt.get();
if (user.getUsername().equals(newUsername)) { if (user.getUsername().equals(newUsername)) {
return new RedirectView("/account?messageType=usernameExists", true); return new RedirectView("/account?messageType=usernameExists", true);
} }
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword", true); return new RedirectView("/account?messageType=incorrectPassword", true);
} }
if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) {
return new RedirectView("/account?messageType=usernameExists", true); return new RedirectView("/account?messageType=usernameExists", true);
} }
if (newUsername != null && newUsername.length() > 0) { if (newUsername != null && newUsername.length() > 0) {
try { try {
userService.changeUsername(user, newUsername); userService.changeUsername(user, newUsername);
@@ -112,10 +103,8 @@ public class UserController {
return new RedirectView("/account?messageType=invalidUsername", true); return new RedirectView("/account?messageType=invalidUsername", true);
} }
} }
// Logout using Spring's utility // Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null); new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
} }
@@ -132,24 +121,18 @@ public class UserController {
if (principal == null) { if (principal == null) {
return new RedirectView("/change-creds?messageType=notAuthenticated", true); return new RedirectView("/change-creds?messageType=notAuthenticated", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName()); Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/change-creds?messageType=userNotFound", true); return new RedirectView("/change-creds?messageType=userNotFound", true);
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/change-creds?messageType=incorrectPassword", true); return new RedirectView("/change-creds?messageType=incorrectPassword", true);
} }
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
userService.changeFirstUse(user, false); userService.changeFirstUse(user, false);
// Logout using Spring's utility // Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null); new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
} }
@@ -166,24 +149,17 @@ public class UserController {
if (principal == null) { if (principal == null) {
return new RedirectView("/account?messageType=notAuthenticated", true); return new RedirectView("/account?messageType=notAuthenticated", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName()); Optional<User> userOpt = userService.findByUsernameIgnoreCase(principal.getName());
if (userOpt == null || userOpt.isEmpty()) { if (userOpt == null || userOpt.isEmpty()) {
return new RedirectView("/account?messageType=userNotFound", true); return new RedirectView("/account?messageType=userNotFound", true);
} }
User user = userOpt.get(); User user = userOpt.get();
if (!userService.isPasswordCorrect(user, currentPassword)) { if (!userService.isPasswordCorrect(user, currentPassword)) {
return new RedirectView("/account?messageType=incorrectPassword", true); return new RedirectView("/account?messageType=incorrectPassword", true);
} }
userService.changePassword(user, newPassword); userService.changePassword(user, newPassword);
// Logout using Spring's utility // Logout using Spring's utility
new SecurityContextLogoutHandler().logout(request, response, null); new SecurityContextLogoutHandler().logout(request, response, null);
return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true); return new RedirectView(LOGIN_MESSAGETYPE_CREDSUPDATED, true);
} }
@@ -193,17 +169,14 @@ public class UserController {
throws IOException { throws IOException {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
updates.put(entry.getKey(), entry.getValue()[0]); updates.put(entry.getKey(), entry.getValue()[0]);
} }
log.debug("Processed updates: " + updates); log.debug("Processed updates: " + updates);
// Assuming you have a method in userService to update the settings for a user // Assuming you have a method in userService to update the settings for a user
userService.updateUserSettings(principal.getName(), updates); userService.updateUserSettings(principal.getName(), updates);
// Redirect to a page of your choice after updating
return "redirect:/account"; // Redirect to a page of your choice after updating return "redirect:/account";
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -216,13 +189,10 @@ public class UserController {
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
if (!userService.isUsernameValid(username)) { if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true); return new RedirectView("/addUsers?messageType=invalidUsername", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
if (user != null && user.getUsername().equalsIgnoreCase(username)) { if (user != null && user.getUsername().equalsIgnoreCase(username)) {
@@ -243,7 +213,6 @@ public class UserController {
// If the role ID is not valid, redirect with an error message // If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role); userService.saveUser(username, AuthenticationType.SSO, role);
} else { } else {
@@ -252,9 +221,9 @@ public class UserController {
} }
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
} }
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true);
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -264,9 +233,7 @@ public class UserController {
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
Authentication authentication) Authentication authentication)
throws IOException { throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
@@ -275,7 +242,6 @@ public class UserController {
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true); return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
@@ -292,11 +258,10 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
User user = userOpt.get(); User user = userOpt.get();
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true);
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -306,9 +271,7 @@ public class UserController {
@RequestParam("enabled") boolean enabled, @RequestParam("enabled") boolean enabled,
Authentication authentication) Authentication authentication)
throws IOException { throws IOException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
@@ -317,15 +280,12 @@ public class UserController {
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true); return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
} }
User user = userOpt.get(); User user = userOpt.get();
userService.changeUserEnabled(user, enabled); userService.changeUserEnabled(user, enabled);
if (!enabled) { if (!enabled) {
// Invalidate all sessions if the user is being disabled // Invalidate all sessions if the user is being disabled
List<Object> principals = sessionRegistry.getAllPrincipals(); List<Object> principals = sessionRegistry.getAllPrincipals();
@@ -349,28 +309,24 @@ public class UserController {
} }
} }
} }
return new RedirectView( return new RedirectView(
"/addUsers", true); // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true);
} }
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/deleteUser/{username}") @PostMapping("/admin/deleteUser/{username}")
public RedirectView deleteUser( public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) { @PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true); return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true); return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
} }
// Invalidate all sessions before deleting the user // Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations = List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(authentication.getPrincipal(), false); sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
@@ -410,6 +366,4 @@ public class UserController {
} }
return ResponseEntity.ok(apiKey); return ResponseEntity.ok(apiKey);
} }
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
} }

View File

@@ -208,7 +208,13 @@ public class ConvertImgPDFController {
String fitOption = request.getFitOption(); String fitOption = request.getFitOption();
String colorType = request.getColorType(); String colorType = request.getColorType();
boolean autoRotate = request.isAutoRotate(); boolean autoRotate = request.isAutoRotate();
// Handle Null entries for formdata
if (colorType == null || colorType.isBlank()) {
colorType = "color";
}
if (fitOption == null || fitOption.isEmpty()) {
fitOption = "fillPage";
}
// Convert the file to PDF and get the resulting bytes // Convert the file to PDF and get the resulting bytes
byte[] bytes = byte[] bytes =
PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory); PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType, pdfDocumentFactory);

View File

@@ -33,6 +33,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertOfficeController { public class ConvertOfficeController {
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { public File convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException {
// Check for valid file extension // Check for valid file extension
String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); String originalFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
@@ -78,13 +85,6 @@ public class ConvertOfficeController {
return fileExtension.matches(extensionPattern); return fileExtension.matches(extensionPattern);
} }
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertOfficeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @PostMapping(consumes = "multipart/form-data", value = "/file/pdf")
@Operation( @Operation(
summary = "Convert a file to a PDF using LibreOffice", summary = "Convert a file to a PDF using LibreOffice",

View File

@@ -6,7 +6,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -26,9 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
// @RequestMapping("/api/v1/convert") // @RequestMapping("/api/v1/convert")
public class ConvertPDFToBookController { public class ConvertPDFToBookController {
@Autowired
@Qualifier("bookAndHtmlFormatsInstalled") @Qualifier("bookAndHtmlFormatsInstalled")
private boolean bookAndHtmlFormatsInstalled; private final boolean bookAndHtmlFormatsInstalled;
public ConvertPDFToBookController(
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) {
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/book") @PostMapping(consumes = "multipart/form-data", value = "/pdf/book")
@Operation( @Operation(
@@ -39,16 +42,13 @@ public class ConvertPDFToBookController {
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request) public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute PdfToBookRequest request)
throws Exception { throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
if (!bookAndHtmlFormatsInstalled) { if (!bookAndHtmlFormatsInstalled) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
"bookAndHtmlFormatsInstalled flag is False, this functionality is not available"); "bookAndHtmlFormatsInstalled flag is False, this functionality is not available");
} }
if (fileInput == null) { if (fileInput == null) {
throw new IllegalArgumentException("Please provide a file for conversion."); throw new IllegalArgumentException("Please provide a file for conversion.");
} }
// Validate the output format // Validate the output format
String outputFormat = request.getOutputFormat().toLowerCase(); String outputFormat = request.getOutputFormat().toLowerCase();
List<String> allowedFormats = List<String> allowedFormats =
@@ -58,28 +58,24 @@ public class ConvertPDFToBookController {
if (!allowedFormats.contains(outputFormat)) { if (!allowedFormats.contains(outputFormat)) {
throw new IllegalArgumentException("Invalid output format: " + outputFormat); throw new IllegalArgumentException("Invalid output format: " + outputFormat);
} }
byte[] outputFileBytes; byte[] outputFileBytes;
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
Path tempOutputFile = Path tempOutputFile =
Files.createTempFile( Files.createTempFile(
"output_", "output_", // Use the output format for the file extension
"." + outputFormat); // Use the output format for the file extension "." + outputFormat);
Path tempInputFile = null; Path tempInputFile = null;
try { try {
// Create temp input file from the provided PDF // Create temp input file from the provided PDF
tempInputFile = Files.createTempFile("input_", ".pdf"); // Assuming input is always PDF // Assuming input is always PDF
tempInputFile = Files.createTempFile("input_", ".pdf");
Files.write(tempInputFile, fileInput.getBytes()); Files.write(tempInputFile, fileInput.getBytes());
command.add("ebook-convert"); command.add("ebook-convert");
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
command.add(tempOutputFile.toString()); command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE) ProcessExecutor.getInstance(ProcessExecutor.Processes.CALIBRE)
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
outputFileBytes = Files.readAllBytes(tempOutputFile); outputFileBytes = Files.readAllBytes(tempOutputFile);
} finally { } finally {
// Clean up temporary files // Clean up temporary files
@@ -88,13 +84,12 @@ public class ConvertPDFToBookController {
} }
Files.deleteIfExists(tempOutputFile); Files.deleteIfExists(tempOutputFile);
} }
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(fileInput.getOriginalFilename()) Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "." + "."
+ outputFormat; // Remove file extension and append .pdf + // Remove file extension and append .pdf
outputFormat;
return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename); return WebResponseUtils.bytesToWebResponse(outputFileBytes, outputFilename);
} }
} }

View File

@@ -46,16 +46,6 @@ public class AutoRenameController {
PDDocument document = Loader.loadPDF(file.getBytes()); PDDocument document = Loader.loadPDF(file.getBytes());
PDFTextStripper reader = PDFTextStripper reader =
new PDFTextStripper() { new PDFTextStripper() {
class LineInfo {
String text;
float fontSize;
LineInfo(String text, float fontSize) {
this.text = text;
this.fontSize = fontSize;
}
}
List<LineInfo> lineInfos = new ArrayList<>(); List<LineInfo> lineInfos = new ArrayList<>();
StringBuilder lineBuilder = new StringBuilder(); StringBuilder lineBuilder = new StringBuilder();
float lastY = -1; float lastY = -1;
@@ -122,6 +112,16 @@ public class AutoRenameController {
.text) .text)
: null); : null);
} }
class LineInfo {
String text;
float fontSize;
LineInfo(String text, float fontSize) {
this.text = text;
this.fontSize = fontSize;
}
}
}; };
String header = reader.getText(document); String header = reader.getText(document);

View File

@@ -23,12 +23,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.google.zxing.BinaryBitmap; import com.google.zxing.*;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.common.HybridBinarizer; import com.google.zxing.common.HybridBinarizer;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
@@ -56,6 +51,52 @@ public class AutoSplitPdfController {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source;
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
source =
new PlanarYUVLuminanceSource(
pixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
byte[] newPixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) {
newPixels[i] = (byte) (pixels[i] & 0xff);
}
source =
new PlanarYUVLuminanceSource(
newPixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else {
throw new IllegalArgumentException(
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
}
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
return result.getText();
} catch (NotFoundException e) {
return null; // there is no QR code in the image
}
}
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation( @Operation(
summary = "Auto split PDF pages into separate documents", summary = "Auto split PDF pages into separate documents",
@@ -154,50 +195,4 @@ public class AutoSplitPdfController {
} }
} }
} }
private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source;
if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) {
byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();
source =
new PlanarYUVLuminanceSource(
pixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) {
int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData();
byte[] newPixels = new byte[pixels.length];
for (int i = 0; i < pixels.length; i++) {
newPixels[i] = (byte) (pixels[i] & 0xff);
}
source =
new PlanarYUVLuminanceSource(
newPixels,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
0,
0,
bufferedImage.getWidth(),
bufferedImage.getHeight(),
false);
} else {
throw new IllegalArgumentException(
"BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data");
}
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
Result result = new MultiFormatReader().decode(bitmap);
return result.getText();
} catch (NotFoundException e) {
return null; // there is no QR code in the image
}
}
} }

View File

@@ -47,6 +47,32 @@ public class BlankPageController {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
public static boolean isBlankImage(
BufferedImage image, int threshold, double whitePercent, int blurSize) {
if (image == null) {
log.info("Error: Image is null");
return false;
}
// Convert to binary image based on the threshold
int whitePixels = 0;
int totalPixels = image.getWidth() * image.getHeight();
for (int i = 0; i < image.getHeight(); i++) {
for (int j = 0; j < image.getWidth(); j++) {
int color = image.getRGB(j, i) & 0xFF;
if (color >= 255 - threshold) {
whitePixels++;
}
}
}
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
return whitePixelPercentage >= whitePercent;
}
@PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks")
@Operation( @Operation(
summary = "Remove blank pages from a PDF file", summary = "Remove blank pages from a PDF file",
@@ -143,30 +169,4 @@ public class BlankPageController {
zos.closeEntry(); zos.closeEntry();
} }
} }
public static boolean isBlankImage(
BufferedImage image, int threshold, double whitePercent, int blurSize) {
if (image == null) {
log.info("Error: Image is null");
return false;
}
// Convert to binary image based on the threshold
int whitePixels = 0;
int totalPixels = image.getWidth() * image.getHeight();
for (int i = 0; i < image.getHeight(); i++) {
for (int j = 0; j < image.getWidth(); j++) {
int color = image.getRGB(j, i) & 0xFF;
if (color >= 255 - threshold) {
whitePixels++;
}
}
}
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100;
log.info(String.format("Page has white pixel percent of %.2f%%", whitePixelPercentage));
return whitePixelPercentage >= whitePercent;
}
} }

View File

@@ -1,6 +1,6 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Image; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.file.Files; import java.nio.file.Files;

View File

@@ -42,6 +42,8 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImageScansController { public class ExtractImageScansController {
private static final String REPLACEFIRST = "[.][^.]+$";
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation( @Operation(
summary = "Extract image scans from an input file", summary = "Extract image scans from an input file",
@@ -221,6 +223,4 @@ public class ExtractImageScansController {
}); });
} }
} }
private static final String REPLACEFIRST = "[.][^.]+$";
} }

View File

@@ -1,19 +1,10 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.AlphaComposite; import java.awt.*;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D; import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D; import java.awt.geom.Path2D;
import java.awt.image.AffineTransformOp; import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.security.SecureRandom; import java.security.SecureRandom;

View File

@@ -13,11 +13,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation; import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;

View File

@@ -1,18 +1,10 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.BufferedReader; import java.io.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
@@ -24,7 +16,6 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
@@ -48,12 +39,14 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
@Slf4j @Slf4j
public class OCRController { public class OCRController {
@Autowired private ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final CustomPDDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired public OCRController(
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) { ApplicationProperties applicationProperties,
CustomPDDocumentFactory pdfDocumentFactory) {
this.applicationProperties = applicationProperties;
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@@ -78,13 +71,11 @@ public class OCRController {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
List<String> languages = request.getLanguages(); List<String> languages = request.getLanguages();
String ocrType = request.getOcrType(); String ocrType = request.getOcrType();
Path tempDir = Files.createTempDirectory("ocr_process"); Path tempDir = Files.createTempDirectory("ocr_process");
Path tempInputFile = tempDir.resolve("input.pdf"); Path tempInputFile = tempDir.resolve("input.pdf");
Path tempOutputDir = tempDir.resolve("output"); Path tempOutputDir = tempDir.resolve("output");
Path tempImagesDir = tempDir.resolve("images"); Path tempImagesDir = tempDir.resolve("images");
Path finalOutputFile = tempDir.resolve("final_output.pdf"); Path finalOutputFile = tempDir.resolve("final_output.pdf");
Files.createDirectories(tempOutputDir); Files.createDirectories(tempOutputDir);
Files.createDirectories(tempImagesDir); Files.createDirectories(tempImagesDir);
Process process = null; Process process = null;
@@ -93,39 +84,32 @@ public class OCRController {
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
PDFMergerUtility merger = new PDFMergerUtility(); PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(finalOutputFile.toString()); merger.setDestinationFileName(finalOutputFile.toString());
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) { try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
int pageCount = document.getNumberOfPages(); int pageCount = document.getNumberOfPages();
for (int pageNum = 0; pageNum < pageCount; pageNum++) { for (int pageNum = 0; pageNum < pageCount; pageNum++) {
PDPage page = document.getPage(pageNum); PDPage page = document.getPage(pageNum);
boolean hasText = false; boolean hasText = false;
// Check for existing text // Check for existing text
try (PDDocument tempDoc = new PDDocument()) { try (PDDocument tempDoc = new PDDocument()) {
tempDoc.addPage(page); tempDoc.addPage(page);
PDFTextStripper stripper = new PDFTextStripper(); PDFTextStripper stripper = new PDFTextStripper();
hasText = !stripper.getText(tempDoc).trim().isEmpty(); hasText = !stripper.getText(tempDoc).trim().isEmpty();
} }
boolean shouldOcr = boolean shouldOcr =
switch (ocrType) { switch (ocrType) {
case "skip-text" -> !hasText; case "skip-text" -> !hasText;
case "force-ocr" -> true; case "force-ocr" -> true;
default -> true; default -> true;
}; };
Path pageOutputPath = Path pageOutputPath =
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum)); tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
if (shouldOcr) { if (shouldOcr) {
// Convert page to image // Convert page to image
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300); BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
Path imagePath = Path imagePath =
tempImagesDir.resolve(String.format("page_%d.png", pageNum)); tempImagesDir.resolve(String.format("page_%d.png", pageNum));
ImageIO.write(image, "png", imagePath.toFile()); ImageIO.write(image, "png", imagePath.toFile());
// Build OCR command // Build OCR command
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("tesseract"); command.add("tesseract");
@@ -136,11 +120,10 @@ public class OCRController {
.toString()); .toString());
command.add("-l"); command.add("-l");
command.add(String.join("+", languages)); command.add(String.join("+", languages));
command.add("pdf"); // Always output PDF // Always output PDF
command.add("pdf");
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
process = pb.start(); process = pb.start();
// Capture any error output // Capture any error output
try (BufferedReader reader = try (BufferedReader reader =
new BufferedReader( new BufferedReader(
@@ -150,13 +133,11 @@ public class OCRController {
log.debug("Tesseract: {}", line); log.debug("Tesseract: {}", line);
} }
} }
int exitCode = process.waitFor(); int exitCode = process.waitFor();
if (exitCode != 0) { if (exitCode != 0) {
throw new RuntimeException( throw new RuntimeException(
"Tesseract failed with exit code: " + exitCode); "Tesseract failed with exit code: " + exitCode);
} }
// Add OCR'd PDF to merger // Add OCR'd PDF to merger
merger.addSource(pageOutputPath.toFile()); merger.addSource(pageOutputPath.toFile());
} else { } else {
@@ -169,29 +150,24 @@ public class OCRController {
} }
} }
} }
// Merge all pages into final PDF // Merge all pages into final PDF
merger.mergeDocuments(null); merger.mergeDocuments(null);
// Read the final PDF file // Read the final PDF file
byte[] pdfContent = Files.readAllBytes(finalOutputFile); byte[] pdfContent = Files.readAllBytes(finalOutputFile);
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_OCR.pdf"; + "_OCR.pdf";
return ResponseEntity.ok() return ResponseEntity.ok()
.header( .header(
"Content-Disposition", "Content-Disposition",
"attachment; filename=\"" + outputFilename + "\"") "attachment; filename=\"" + outputFilename + "\"")
.contentType(MediaType.APPLICATION_PDF) .contentType(MediaType.APPLICATION_PDF)
.body(pdfContent); .body(pdfContent);
} finally { } finally {
if (process != null) { if (process != null) {
process.destroy(); process.destroy();
} }
// Clean up temporary files // Clean up temporary files
deleteDirectory(tempDir); deleteDirectory(tempDir);
} }
@@ -203,17 +179,14 @@ public class OCRController {
log.warn("File {} does not exist, skipping", file); log.warn("File {} does not exist, skipping", file);
return; return;
} }
try (FileInputStream fis = new FileInputStream(file)) { try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(filename); ZipEntry zipEntry = new ZipEntry(filename);
zipOut.putNextEntry(zipEntry); zipOut.putNextEntry(zipEntry);
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int length; int length;
while ((length = fis.read(buffer)) >= 0) { while ((length = fis.read(buffer)) >= 0) {
zipOut.write(buffer, 0, length); zipOut.write(buffer, 0, length);
} }
zipOut.closeEntry(); zipOut.closeEntry();
} }
} }

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Graphics; import java.awt.*;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.print.PageFormat; import java.awt.print.PageFormat;
import java.awt.print.Printable; import java.awt.print.Printable;

View File

@@ -1,6 +1,6 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.Color; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;

View File

@@ -30,17 +30,24 @@ public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>(); private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
@Autowired private ServletContext servletContext; private final ServletContext servletContext;
private final UserServiceInterface userService;
Map<String, List<String>> outputToFileTypes = new HashMap<>();
JsonNode apiDocsJsonRootNode;
public ApiDocService(
ServletContext servletContext,
@Autowired(required = false) UserServiceInterface userService) {
this.servletContext = servletContext;
this.userService = userService;
}
private String getApiDocsUrl() { private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort(); String port = SPdfApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/v1/api-docs"; return "http://localhost:" + port + contextPath + "/v1/api-docs";
} }
Map<String, List<String>> outputToFileTypes = new HashMap<>();
public List<String> getExtensionTypes(boolean output, String operationName) { public List<String> getExtensionTypes(boolean output, String operationName) {
if (outputToFileTypes.size() == 0) { if (outputToFileTypes.size() == 0) {
outputToFileTypes.put("PDF", Arrays.asList("pdf")); outputToFileTypes.put("PDF", Arrays.asList("pdf"));
@@ -64,14 +71,12 @@ public class ApiDocService {
"BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx")); "BOOK", Arrays.asList("epub", "mobi", "azw3", "fb2", "txt", "docx"));
// type. // type.
} }
if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) { if (apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
loadApiDocumentation(); loadApiDocumentation();
} }
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return null; return null;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
String description = endpoint.getDescription(); String description = endpoint.getDescription();
Pattern pattern = null; Pattern pattern = null;
@@ -90,16 +95,11 @@ public class ApiDocService {
return null; return null;
} }
@Autowired(required = false)
private UserServiceInterface userService;
private String getApiKeyForUser() { private String getApiKeyForUser() {
if (userService == null) return ""; if (userService == null) return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId()); return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
} }
JsonNode apiDocsJsonRootNode;
// @EventListener(ApplicationReadyEvent.class) // @EventListener(ApplicationReadyEvent.class)
private synchronized void loadApiDocumentation() { private synchronized void loadApiDocumentation() {
String apiDocsJson = ""; String apiDocsJson = "";
@@ -110,15 +110,12 @@ public class ApiDocService {
headers.set("X-API-KEY", apiKey); headers.set("X-API-KEY", apiKey);
} }
HttpEntity<String> entity = new HttpEntity<>(headers); HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = ResponseEntity<String> response =
restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class); restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody(); apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson); apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths"); JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.fields() paths.fields()
.forEachRemaining( .forEachRemaining(
@@ -155,19 +152,15 @@ public class ApiDocService {
if (!apiDocumentation.containsKey(operationName)) { if (!apiDocumentation.containsKey(operationName)) {
return false; return false;
} }
ApiEndpoint endpoint = apiDocumentation.get(operationName); ApiEndpoint endpoint = apiDocumentation.get(operationName);
String description = endpoint.getDescription(); String description = endpoint.getDescription();
Pattern pattern = Pattern.compile("Type:(\\w+)"); Pattern pattern = Pattern.compile("Type:(\\w+)");
Matcher matcher = pattern.matcher(description); Matcher matcher = pattern.matcher(description);
if (matcher.find()) { if (matcher.find()) {
String type = matcher.group(1); String type = matcher.group(1);
return type.startsWith("MI"); return type.startsWith("MI");
} }
return false; return false;
} }
} }
// Model class for API Endpoint // Model class for API Endpoint

View File

@@ -8,7 +8,6 @@ import java.util.Map;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -37,17 +36,27 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class PipelineController { public class PipelineController {
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired PipelineProcessor processor;
@Autowired ApplicationProperties applicationProperties; private final PipelineProcessor processor;
@Autowired private ObjectMapper objectMapper; private final ApplicationProperties applicationProperties;
private final ObjectMapper objectMapper;
public PipelineController(
PipelineProcessor processor,
ApplicationProperties applicationProperties,
ObjectMapper objectMapper) {
this.processor = processor;
this.applicationProperties = applicationProperties;
this.objectMapper = objectMapper;
}
@PostMapping("/handleData") @PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request)
throws JsonMappingException, JsonProcessingException { throws JsonMappingException, JsonProcessingException {
MultipartFile[] files = request.getFileInput(); MultipartFile[] files = request.getFileInput();
String jsonString = request.getJson(); String jsonString = request.getJson();
if (files == null) { if (files == null) {
@@ -68,26 +77,21 @@ public class PipelineController {
byte[] bytes = new byte[(int) singleFile.contentLength()]; byte[] bytes = new byte[(int) singleFile.contentLength()];
is.read(bytes); is.read(bytes);
is.close(); is.close();
log.info("Returning single file response..."); log.info("Returning single file response...");
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM);
} else if (outputFiles == null) { } else if (outputFiles == null) {
return null; return null;
} }
// Create a ByteArrayOutputStream to hold the zip // Create a ByteArrayOutputStream to hold the zip
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(baos); ZipOutputStream zipOut = new ZipOutputStream(baos);
// A map to keep track of filenames and their counts // A map to keep track of filenames and their counts
Map<String, Integer> filenameCount = new HashMap<>(); Map<String, Integer> filenameCount = new HashMap<>();
// Loop through each file and add it to the zip // Loop through each file and add it to the zip
for (Resource file : outputFiles) { for (Resource file : outputFiles) {
String originalFilename = file.getFilename(); String originalFilename = file.getFilename();
String filename = originalFilename; String filename = originalFilename;
// Check if the filename already exists, and modify it if necessary // Check if the filename already exists, and modify it if necessary
if (filenameCount.containsKey(originalFilename)) { if (filenameCount.containsKey(originalFilename)) {
int count = filenameCount.get(originalFilename); int count = filenameCount.get(originalFilename);
@@ -98,24 +102,18 @@ public class PipelineController {
} else { } else {
filenameCount.put(originalFilename, 1); filenameCount.put(originalFilename, 1);
} }
ZipEntry zipEntry = new ZipEntry(filename); ZipEntry zipEntry = new ZipEntry(filename);
zipOut.putNextEntry(zipEntry); zipOut.putNextEntry(zipEntry);
// Read the file into a byte array // Read the file into a byte array
InputStream is = file.getInputStream(); InputStream is = file.getInputStream();
byte[] bytes = new byte[(int) file.contentLength()]; byte[] bytes = new byte[(int) file.contentLength()];
is.read(bytes); is.read(bytes);
// Write the bytes of the file to the zip // Write the bytes of the file to the zip
zipOut.write(bytes, 0, bytes.length); zipOut.write(bytes, 0, bytes.length);
zipOut.closeEntry(); zipOut.closeEntry();
is.close(); is.close();
} }
zipOut.close(); zipOut.close();
log.info("Returning zipped file response..."); log.info("Returning zipped file response...");
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.boasToWebResponse(
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);

View File

@@ -16,7 +16,6 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -34,19 +33,31 @@ import stirling.software.SPDF.utils.FileMonitor;
@Slf4j @Slf4j
public class PipelineDirectoryProcessor { public class PipelineDirectoryProcessor {
@Autowired private ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@Autowired private ApiDocService apiDocService;
@Autowired PipelineProcessor processor;
@Autowired FileMonitor fileMonitor;
final String watchedFoldersDir; private final ApiDocService apiDocService;
final String finishedFoldersDir;
private final PipelineProcessor processor;
private final FileMonitor fileMonitor;
private final String watchedFoldersDir;
private final String finishedFoldersDir;
public PipelineDirectoryProcessor( public PipelineDirectoryProcessor(
ObjectMapper objectMapper,
ApiDocService apiDocService,
@Qualifier("watchedFoldersDir") String watchedFoldersDir, @Qualifier("watchedFoldersDir") String watchedFoldersDir,
@Qualifier("finishedFoldersDir") String finishedFoldersDir) { @Qualifier("finishedFoldersDir") String finishedFoldersDir,
PipelineProcessor processor,
FileMonitor fileMonitor) {
this.objectMapper = objectMapper;
this.apiDocService = apiDocService;
this.watchedFoldersDir = watchedFoldersDir; this.watchedFoldersDir = watchedFoldersDir;
this.finishedFoldersDir = finishedFoldersDir; this.finishedFoldersDir = finishedFoldersDir;
this.processor = processor;
this.fileMonitor = fileMonitor;
} }
@Scheduled(fixedRate = 60000) @Scheduled(fixedRate = 60000)
@@ -81,13 +92,11 @@ public class PipelineDirectoryProcessor {
public void handleDirectory(Path dir) throws IOException { public void handleDirectory(Path dir) throws IOException {
log.info("Handling directory: {}", dir); log.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir); Path processingDir = createProcessingDirectory(dir);
Optional<Path> jsonFileOptional = findJsonFile(dir); Optional<Path> jsonFileOptional = findJsonFile(dir);
if (!jsonFileOptional.isPresent()) { if (!jsonFileOptional.isPresent()) {
log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir); log.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
return; return;
} }
Path jsonFile = jsonFileOptional.get(); Path jsonFile = jsonFileOptional.get();
PipelineConfig config = readAndParseJson(jsonFile); PipelineConfig config = readAndParseJson(jsonFile);
processPipelineOperations(dir, processingDir, jsonFile, config); processPipelineOperations(dir, processingDir, jsonFile, config);
@@ -166,13 +175,11 @@ public class PipelineDirectoryProcessor {
private Path resolveUniqueFilePath(Path directory, String originalFileName) { private Path resolveUniqueFilePath(Path directory, String originalFileName) {
Path filePath = directory.resolve(originalFileName); Path filePath = directory.resolve(originalFileName);
int counter = 1; int counter = 1;
while (Files.exists(filePath)) { while (Files.exists(filePath)) {
String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")"); String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")");
filePath = directory.resolve(newName); filePath = directory.resolve(newName);
counter++; counter++;
} }
return filePath; return filePath;
} }
@@ -211,17 +218,14 @@ public class PipelineDirectoryProcessor {
for (Resource resource : resources) { for (Resource resource : resources) {
String outputFileName = createOutputFileName(resource, config); String outputFileName = createOutputFileName(resource, config);
Path outputPath = determineOutputPath(config, dir); Path outputPath = determineOutputPath(config, dir);
if (!Files.exists(outputPath)) { if (!Files.exists(outputPath)) {
Files.createDirectories(outputPath); Files.createDirectories(outputPath);
log.info("Created directory: {}", outputPath); log.info("Created directory: {}", outputPath);
} }
Path outputFile = outputPath.resolve(outputFileName); Path outputFile = outputPath.resolve(outputFileName);
try (OutputStream os = new FileOutputStream(outputFile.toFile())) { try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
os.write(((ByteArrayResource) resource).getByteArray()); os.write(((ByteArrayResource) resource).getByteArray());
} }
log.info("File moved and renamed to {}", outputFile); log.info("File moved and renamed to {}", outputFile);
} }
} }
@@ -230,7 +234,6 @@ public class PipelineDirectoryProcessor {
String resourceName = resource.getFilename(); String resourceName = resource.getFilename();
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.')); String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1); String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
String outputFileName = String outputFileName =
config.getOutputPattern() config.getOutputPattern()
.replace("{filename}", baseName) .replace("{filename}", baseName)
@@ -245,7 +248,6 @@ public class PipelineDirectoryProcessor {
.format(DateTimeFormatter.ofPattern("HHmmss"))) .format(DateTimeFormatter.ofPattern("HHmmss")))
+ "." + "."
+ extension; + extension;
return outputFileName; return outputFileName;
} }
@@ -255,7 +257,6 @@ public class PipelineDirectoryProcessor {
.replace("{outputFolder}", finishedFoldersDir) .replace("{outputFolder}", finishedFoldersDir)
.replace("{folderName}", dir.toString()) .replace("{folderName}", dir.toString())
.replaceAll("\\\\?watchedFolders", ""); .replaceAll("\\\\?watchedFolders", "");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir); return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
} }

View File

@@ -1,10 +1,6 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@@ -22,12 +18,7 @@ import java.util.zip.ZipInputStream;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity; import org.springframework.http.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
@@ -48,12 +39,39 @@ import stirling.software.SPDF.model.Role;
@Slf4j @Slf4j
public class PipelineProcessor { public class PipelineProcessor {
@Autowired private ApiDocService apiDocService; private final ApiDocService apiDocService;
@Autowired(required = false) private final UserServiceInterface userService;
private UserServiceInterface userService;
@Autowired private ServletContext servletContext; private final ServletContext servletContext;
public PipelineProcessor(
ApiDocService apiDocService,
@Autowired(required = false) UserServiceInterface userService,
ServletContext servletContext) {
this.apiDocService = apiDocService;
this.userService = userService;
this.servletContext = servletContext;
}
public static String removeTrailingNaming(String filename) {
// Splitting filename into name and extension
int dotIndex = filename.lastIndexOf(".");
if (dotIndex == -1) {
// No extension found
return filename;
}
String name = filename.substring(0, dotIndex);
String extension = filename.substring(dotIndex);
// Finding the last underscore
int underscoreIndex = name.lastIndexOf("_");
if (underscoreIndex == -1) {
// No underscore found
return filename;
}
// Removing the last part and reattaching the extension
return name.substring(0, underscoreIndex) + extension;
}
private String getApiKeyForUser() { private String getApiKeyForUser() {
if (userService == null) return ""; if (userService == null) return "";
@@ -63,22 +81,17 @@ public class PipelineProcessor {
private String getBaseUrl() { private String getBaseUrl() {
String contextPath = servletContext.getContextPath(); String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getStaticPort(); String port = SPdfApplication.getStaticPort();
return "http://localhost:" + port + contextPath + "/"; return "http://localhost:" + port + contextPath + "/";
} }
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config) List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config)
throws Exception { throws Exception {
ByteArrayOutputStream logStream = new ByteArrayOutputStream(); ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream); PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false; boolean hasErrors = false;
for (PipelineOperation pipelineOperation : config.getOperations()) { for (PipelineOperation pipelineOperation : config.getOperations()) {
String operation = pipelineOperation.getOperation(); String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation); boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
log.info( log.info(
"Running operation: {} isMultiInputOperation {}", "Running operation: {} isMultiInputOperation {}",
operation, operation,
@@ -89,9 +102,7 @@ public class PipelineProcessor {
inputFileTypes = new ArrayList<String>(Arrays.asList("ALL")); inputFileTypes = new ArrayList<String>(Arrays.asList("ALL"));
} }
// List outputFileTypes = apiDocService.getExtensionTypes(true, operation); // List outputFileTypes = apiDocService.getExtensionTypes(true, operation);
String url = getBaseUrl() + operation; String url = getBaseUrl() + operation;
List<Resource> newOutputFiles = new ArrayList<>(); List<Resource> newOutputFiles = new ArrayList<>();
if (!isMultiInputOperation) { if (!isMultiInputOperation) {
for (Resource file : outputFiles) { for (Resource file : outputFiles) {
@@ -101,7 +112,6 @@ public class PipelineProcessor {
hasInputFileType = true; hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file); body.add("fileInput", file);
for (Entry<String, Object> entry : parameters.entrySet()) { for (Entry<String, Object> entry : parameters.entrySet()) {
if (entry.getValue() instanceof List) { if (entry.getValue() instanceof List) {
List<?> list = (List<?>) entry.getValue(); List<?> list = (List<?>) entry.getValue();
@@ -112,9 +122,7 @@ public class PipelineProcessor {
body.add(entry.getKey(), entry.getValue()); body.add(entry.getKey(), entry.getValue());
} }
} }
ResponseEntity<byte[]> response = sendWebRequest(url, body); ResponseEntity<byte[]> response = sendWebRequest(url, body);
// If the operation is filter and the response body is null or empty, // If the operation is filter and the response body is null or empty,
// skip // skip
// this // this
@@ -125,7 +133,6 @@ public class PipelineProcessor {
log.info("Skipping file due to failing {}", operation); log.info("Skipping file due to failing {}", operation);
continue; continue;
} }
if (!response.getStatusCode().equals(HttpStatus.OK)) { if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody()); logPrintStream.println("Error: " + response.getBody());
hasErrors = true; hasErrors = true;
@@ -134,7 +141,6 @@ public class PipelineProcessor {
processOutputFiles(operation, response, newOutputFiles); processOutputFiles(operation, response, newOutputFiles);
} }
} }
if (!hasInputFileType) { if (!hasInputFileType) {
logPrintStream.println( logPrintStream.println(
"No files with extension " "No files with extension "
@@ -144,7 +150,6 @@ public class PipelineProcessor {
hasErrors = true; hasErrors = true;
} }
} }
} else { } else {
// Filter and collect all files that match the inputFileExtension // Filter and collect all files that match the inputFileExtension
List<Resource> matchingFiles; List<Resource> matchingFiles;
@@ -160,17 +165,14 @@ public class PipelineProcessor {
.anyMatch(file.getFilename()::endsWith)) .anyMatch(file.getFilename()::endsWith))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
// Check if there are matching files // Check if there are matching files
if (!matchingFiles.isEmpty()) { if (!matchingFiles.isEmpty()) {
// Create a new MultiValueMap for the request body // Create a new MultiValueMap for the request body
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// Add all matching files to the body // Add all matching files to the body
for (Resource file : matchingFiles) { for (Resource file : matchingFiles) {
body.add("fileInput", file); body.add("fileInput", file);
} }
for (Entry<String, Object> entry : parameters.entrySet()) { for (Entry<String, Object> entry : parameters.entrySet()) {
if (entry.getValue() instanceof List) { if (entry.getValue() instanceof List) {
List<?> list = (List<?>) entry.getValue(); List<?> list = (List<?>) entry.getValue();
@@ -181,9 +183,7 @@ public class PipelineProcessor {
body.add(entry.getKey(), entry.getValue()); body.add(entry.getKey(), entry.getValue());
} }
} }
ResponseEntity<byte[]> response = sendWebRequest(url, body); ResponseEntity<byte[]> response = sendWebRequest(url, body);
// Handle the response // Handle the response
if (response.getStatusCode().equals(HttpStatus.OK)) { if (response.getStatusCode().equals(HttpStatus.OK)) {
processOutputFiles(operation, response, newOutputFiles); processOutputFiles(operation, response, newOutputFiles);
@@ -208,48 +208,22 @@ public class PipelineProcessor {
if (hasErrors) { if (hasErrors) {
log.error("Errors occurred during processing. Log: {}", logStream.toString()); log.error("Errors occurred during processing. Log: {}", logStream.toString());
} }
return outputFiles; return outputFiles;
} }
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) { private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key // Set up headers, including API key
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-KEY", apiKey); headers.add("X-API-KEY", apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create HttpEntity with the body and headers // Create HttpEntity with the body and headers
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers); HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// Make the request to the REST endpoint // Make the request to the REST endpoint
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
} }
public static String removeTrailingNaming(String filename) {
// Splitting filename into name and extension
int dotIndex = filename.lastIndexOf(".");
if (dotIndex == -1) {
// No extension found
return filename;
}
String name = filename.substring(0, dotIndex);
String extension = filename.substring(dotIndex);
// Finding the last underscore
int underscoreIndex = name.lastIndexOf("_");
if (underscoreIndex == -1) {
// No underscore found
return filename;
}
// Removing the last part and reattaching the extension
return name.substring(0, underscoreIndex) + extension;
}
private List<Resource> processOutputFiles( private List<Resource> processOutputFiles(
String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) String operation, ResponseEntity<byte[]> response, List<Resource> newOutputFiles)
throws IOException { throws IOException {
@@ -259,13 +233,11 @@ public class PipelineProcessor {
// If the operation is "auto-rename", generate a new filename. // If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp. // This is a simple example of generating a filename using current timestamp.
// Modify as per your needs. // Modify as per your needs.
newFilename = extractFilename(response); newFilename = extractFilename(response);
} else { } else {
// Otherwise, keep the original filename. // Otherwise, keep the original filename.
newFilename = removeTrailingNaming(extractFilename(response)); newFilename = removeTrailingNaming(extractFilename(response));
} }
// Check if the response body is a zip file // Check if the response body is a zip file
if (isZip(response.getBody())) { if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files // Unzip the file and add all the files to the new output files
@@ -273,6 +245,7 @@ public class PipelineProcessor {
} else { } else {
Resource outputResource = Resource outputResource =
new ByteArrayResource(response.getBody()) { new ByteArrayResource(response.getBody()) {
@Override @Override
public String getFilename() { public String getFilename() {
return newFilename; return newFilename;
@@ -280,16 +253,14 @@ public class PipelineProcessor {
}; };
newOutputFiles.add(outputResource); newOutputFiles.add(outputResource);
} }
return newOutputFiles; return newOutputFiles;
} }
public String extractFilename(ResponseEntity<byte[]> response) { public String extractFilename(ResponseEntity<byte[]> response) {
String filename = "default-filename.ext"; // Default filename if not found // Default filename if not found
String filename = "default-filename.ext";
HttpHeaders headers = response.getHeaders(); HttpHeaders headers = response.getHeaders();
String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION); String contentDisposition = headers.getFirst(HttpHeaders.CONTENT_DISPOSITION);
if (contentDisposition != null && !contentDisposition.isEmpty()) { if (contentDisposition != null && !contentDisposition.isEmpty()) {
String[] parts = contentDisposition.split(";"); String[] parts = contentDisposition.split(";");
for (String part : parts) { for (String part : parts) {
@@ -297,12 +268,10 @@ public class PipelineProcessor {
// Extracts filename and removes quotes if present // Extracts filename and removes quotes if present
filename = part.split("=")[1].trim().replace("\"", ""); filename = part.split("=")[1].trim().replace("\"", "");
filename = URLDecoder.decode(filename, StandardCharsets.UTF_8); filename = URLDecoder.decode(filename, StandardCharsets.UTF_8);
break; break;
} }
} }
} }
return filename; return filename;
} }
@@ -311,16 +280,15 @@ public class PipelineProcessor {
log.info("No files"); log.info("No files");
return null; return null;
} }
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (File file : files) { for (File file : files) {
Path path = Paths.get(file.getAbsolutePath()); Path path = Paths.get(file.getAbsolutePath());
log.info("Reading file: " + path); // debug statement // debug statement
log.info("Reading file: " + path);
if (Files.exists(path)) { if (Files.exists(path)) {
Resource fileResource = Resource fileResource =
new ByteArrayResource(Files.readAllBytes(path)) { new ByteArrayResource(Files.readAllBytes(path)) {
@Override @Override
public String getFilename() { public String getFilename() {
return file.getName(); return file.getName();
@@ -340,12 +308,11 @@ public class PipelineProcessor {
log.info("No files"); log.info("No files");
return null; return null;
} }
List<Resource> outputFiles = new ArrayList<>(); List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) { for (MultipartFile file : files) {
Resource fileResource = Resource fileResource =
new ByteArrayResource(file.getBytes()) { new ByteArrayResource(file.getBytes()) {
@Override @Override
public String getFilename() { public String getFilename() {
return Filenames.toSimpleFileName(file.getOriginalFilename()); return Filenames.toSimpleFileName(file.getOriginalFilename());
@@ -361,7 +328,6 @@ public class PipelineProcessor {
if (data == null || data.length < 4) { if (data == null || data.length < 4) {
return false; return false;
} }
// Check the first four bytes of the data against the standard zip magic number // Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04; return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
} }
@@ -369,29 +335,25 @@ public class PipelineProcessor {
private List<Resource> unzip(byte[] data) throws IOException { private List<Resource> unzip(byte[] data) throws IOException {
log.info("Unzipping data of length: {}", data.length); log.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>(); List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data); try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) { ZipInputStream zis = ZipSecurity.createHardenedInputStream(bais)) {
ZipEntry entry; ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) { while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024]; byte[] buffer = new byte[1024];
int count; int count;
while ((count = zis.read(buffer)) != -1) { while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count); baos.write(buffer, 0, count);
} }
final String filename = entry.getName(); final String filename = entry.getName();
Resource fileResource = Resource fileResource =
new ByteArrayResource(baos.toByteArray()) { new ByteArrayResource(baos.toByteArray()) {
@Override @Override
public String getFilename() { public String getFilename() {
return filename; return filename;
} }
}; };
// If the unzipped file is a zip file, unzip it // If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) { if (isZip(baos.toByteArray())) {
log.info("File {} is a zip file. Unzipping...", filename); log.info("File {} is a zip file. Unzipping...", filename);
@@ -401,7 +363,6 @@ public class PipelineProcessor {
} }
} }
} }
log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size()); log.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles; return unzippedFiles;
} }

View File

@@ -1,20 +1,9 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.*;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.KeyStore; import java.security.*;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.CertificateException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
@@ -91,6 +80,151 @@ public class CertSignController {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
private static void sign(
CustomPDDocumentFactory pdfDocumentFactory,
byte[] input,
OutputStream output,
CreateSignature instance,
Boolean showSignature,
Integer pageNumber,
String name,
String location,
String reason,
Boolean showLogo) {
try (PDDocument doc = pdfDocumentFactory.load(input)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions);
} else {
doc.addSignature(signature, instance);
}
doc.saveIncremental(output);
} catch (Exception e) {
log.error("exception", e);
}
}
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
+ " Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
MultipartFile jksfile = request.getJksFile();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber() - 1;
Boolean showLogo = request.isShowLogo();
if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided");
}
KeyStore ks = null;
switch (certType) {
case "PEM":
ks = KeyStore.getInstance("JKS");
ks.load(null);
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
ks.setKeyEntry(
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
break;
case "PKCS12":
ks = KeyStore.getInstance("PKCS12");
ks.load(p12File.getInputStream(), password.toCharArray());
break;
case "JKS":
ks = KeyStore.getInstance("JKS");
ks.load(jksfile.getInputStream(), password.toCharArray());
break;
default:
throw new IllegalArgumentException("Invalid cert type: " + certType);
}
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(
pdfDocumentFactory,
pdf.getBytes(),
baos,
createSignature,
showSignature,
pageNumber,
name,
location,
reason,
showLogo);
return WebResponseUtils.boasToWebResponse(
baos,
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
+ "_signed.pdf");
}
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
throws IOException, OperatorCreationException, PKCSException {
try (PEMParser pemParser =
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
Object pemObject = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKeyInfo pkInfo;
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
InputDecryptorProvider decProv =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
} else if (pemObject instanceof PEMEncryptedKeyPair) {
PEMDecryptorProvider decProv =
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
pkInfo =
((PEMEncryptedKeyPair) pemObject)
.decryptKeyPair(decProv)
.getPrivateKeyInfo();
} else {
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
}
return converter.getPrivateKey(pkInfo);
}
}
private Certificate getCertificateFromPEM(byte[] pemBytes)
throws IOException, CertificateException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) {
return CertificateFactory.getInstance("X.509").generateCertificate(bis);
}
}
class CreateSignature extends CreateSignatureBase { class CreateSignature extends CreateSignatureBase {
File logoFile; File logoFile;
@@ -198,149 +332,4 @@ public class CertSignController {
} }
} }
} }
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public CertSignController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign")
@Operation(
summary = "Sign PDF with a Digital Certificate",
description =
"This endpoint accepts a PDF file, a digital certificate and related information to sign"
+ " the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF"
+ " Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
MultipartFile certFile = request.getCertFile();
MultipartFile p12File = request.getP12File();
MultipartFile jksfile = request.getJksFile();
String password = request.getPassword();
Boolean showSignature = request.isShowSignature();
String reason = request.getReason();
String location = request.getLocation();
String name = request.getName();
Integer pageNumber = request.getPageNumber() - 1;
Boolean showLogo = request.isShowLogo();
if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided");
}
KeyStore ks = null;
switch (certType) {
case "PEM":
ks = KeyStore.getInstance("JKS");
ks.load(null);
PrivateKey privateKey = getPrivateKeyFromPEM(privateKeyFile.getBytes(), password);
Certificate cert = (Certificate) getCertificateFromPEM(certFile.getBytes());
ks.setKeyEntry(
"alias", privateKey, password.toCharArray(), new Certificate[] {cert});
break;
case "PKCS12":
ks = KeyStore.getInstance("PKCS12");
ks.load(p12File.getInputStream(), password.toCharArray());
break;
case "JKS":
ks = KeyStore.getInstance("JKS");
ks.load(jksfile.getInputStream(), password.toCharArray());
break;
default:
throw new IllegalArgumentException("Invalid cert type: " + certType);
}
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(
pdfDocumentFactory,
pdf.getBytes(),
baos,
createSignature,
showSignature,
pageNumber,
name,
location,
reason,
showLogo);
return WebResponseUtils.boasToWebResponse(
baos,
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
+ "_signed.pdf");
}
private static void sign(
CustomPDDocumentFactory pdfDocumentFactory,
byte[] input,
OutputStream output,
CreateSignature instance,
Boolean showSignature,
Integer pageNumber,
String name,
String location,
String reason,
Boolean showLogo) {
try (PDDocument doc = pdfDocumentFactory.load(input)) {
PDSignature signature = new PDSignature();
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
signature.setName(name);
signature.setLocation(location);
signature.setReason(reason);
signature.setSignDate(Calendar.getInstance());
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions);
} else {
doc.addSignature(signature, instance);
}
doc.saveIncremental(output);
} catch (Exception e) {
log.error("exception", e);
}
}
private PrivateKey getPrivateKeyFromPEM(byte[] pemBytes, String password)
throws IOException, OperatorCreationException, PKCSException {
try (PEMParser pemParser =
new PEMParser(new InputStreamReader(new ByteArrayInputStream(pemBytes)))) {
Object pemObject = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKeyInfo pkInfo;
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
InputDecryptorProvider decProv =
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(password.toCharArray());
pkInfo = ((PKCS8EncryptedPrivateKeyInfo) pemObject).decryptPrivateKeyInfo(decProv);
} else if (pemObject instanceof PEMEncryptedKeyPair) {
PEMDecryptorProvider decProv =
new JcePEMDecryptorProviderBuilder().build(password.toCharArray());
pkInfo =
((PEMEncryptedKeyPair) pemObject)
.decryptKeyPair(decProv)
.getPrivateKeyInfo();
} else {
pkInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
}
return converter.getPrivateKey(pkInfo);
}
}
private Certificate getCertificateFromPEM(byte[] pemBytes)
throws IOException, CertificateException {
try (ByteArrayInputStream bis = new ByteArrayInputStream(pemBytes)) {
return CertificateFactory.getInstance("X.509").generateCertificate(bis);
}
}
} }

View File

@@ -4,25 +4,13 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Calendar; import java.util.*;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.PDJavascriptNameTreeNode;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.common.PDStream;
@@ -83,6 +71,48 @@ public class GetInfoOnPDF {
static ObjectMapper objectMapper = new ObjectMapper(); static ObjectMapper objectMapper = new ObjectMapper();
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return;
ObjectNode outlineNode = objectMapper.createObjectNode();
outlineNode.put("Title", outline.getTitle());
// You can add other properties if needed
arrayNode.add(outlineNode);
PDOutlineItem child = outline.getFirstChild();
while (child != null) {
addOutlinesToArray(child, arrayNode);
child = child.getNextSibling();
}
}
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
// Check XMP Metadata
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
COSInputStream metaStream = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
if (xmpString.contains(standardKeyword)) {
return true;
}
}
} catch (
Exception
e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions.
log.error("exception", e);
}
return false;
}
@PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf")
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException { public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
@@ -606,21 +636,6 @@ public class GetInfoOnPDF {
return state ? "Allowed" : "Not Allowed"; return state ? "Allowed" : "Not Allowed";
} }
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return;
ObjectNode outlineNode = objectMapper.createObjectNode();
outlineNode.put("Title", outline.getTitle());
// You can add other properties if needed
arrayNode.add(outlineNode);
PDOutlineItem child = outline.getFirstChild();
while (child != null) {
addOutlinesToArray(child, arrayNode);
child = child.getNextSibling();
}
}
public String getPageOrientation(double width, double height) { public String getPageOrientation(double width, double height) {
if (width > height) { if (width > height) {
return "Landscape"; return "Landscape";
@@ -678,33 +693,6 @@ public class GetInfoOnPDF {
return dimensionInfo; return dimensionInfo;
} }
public static boolean checkForStandard(PDDocument document, String standardKeyword) {
// Check XMP Metadata
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
COSInputStream metaStream = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
if (xmpString.contains(standardKeyword)) {
return true;
}
}
} catch (
Exception
e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions.
log.error("exception", e);
}
return false;
}
public ArrayNode exploreStructureTree(List<Object> nodes) { public ArrayNode exploreStructureTree(List<Object> nodes) {
ArrayNode elementsArray = objectMapper.createArrayNode(); ArrayNode elementsArray = objectMapper.createArrayNode();
if (nodes != null) { if (nodes != null) {

View File

@@ -1,6 +1,6 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.*;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;

View File

@@ -4,17 +4,9 @@ import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction; import org.apache.pdfbox.pdmodel.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionLaunch;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.action.PDFormFieldAdditionalActions;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;

View File

@@ -14,11 +14,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder; import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSProcessable; import org.bouncycastle.cms.*;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder; import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.util.Store; import org.bouncycastle.util.Store;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -1,6 +1,6 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.Color; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;

View File

@@ -5,7 +5,6 @@ import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@@ -38,24 +37,30 @@ import stirling.software.SPDF.repository.UserRepository;
@Tag(name = "Account Security", description = "Account Security APIs") @Tag(name = "Account Security", description = "Account Security APIs")
public class AccountWebController { public class AccountWebController {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired SessionPersistentRegistry sessionPersistentRegistry;
@Autowired private final SessionPersistentRegistry sessionPersistentRegistry;
private UserRepository userRepository; // Assuming you have a repository for user operations
private final UserRepository // Assuming you have a repository for user operations
userRepository;
public AccountWebController(
ApplicationProperties applicationProperties,
SessionPersistentRegistry sessionPersistentRegistry,
UserRepository userRepository) {
this.applicationProperties = applicationProperties;
this.sessionPersistentRegistry = sessionPersistentRegistry;
this.userRepository = userRepository;
}
@GetMapping("/login") @GetMapping("/login")
public String login(HttpServletRequest request, Model model, Authentication authentication) { public String login(HttpServletRequest request, Model model, Authentication authentication) {
// If the user is already authenticated, redirect them to the home page. // If the user is already authenticated, redirect them to the home page.
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
return "redirect:/"; return "redirect:/";
} }
Map<String, String> providerList = new HashMap<>(); Map<String, String> providerList = new HashMap<>();
Security securityProps = applicationProperties.getSecurity(); Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2(); OAUTH2 oauth = securityProps.getOauth2();
if (oauth != null) { if (oauth != null) {
if (oauth.getEnabled()) { if (oauth.getEnabled()) {
@@ -70,14 +75,12 @@ public class AccountWebController {
"/oauth2/authorization/" + google.getName(), "/oauth2/authorization/" + google.getName(),
google.getClientName()); google.getClientName());
} }
GithubProvider github = client.getGithub(); GithubProvider github = client.getGithub();
if (github.isSettingsValid()) { if (github.isSettingsValid()) {
providerList.put( providerList.put(
"/oauth2/authorization/" + github.getName(), "/oauth2/authorization/" + github.getName(),
github.getClientName()); github.getClientName());
} }
KeycloakProvider keycloak = client.getKeycloak(); KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) { if (keycloak.isSettingsValid()) {
providerList.put( providerList.put(
@@ -87,7 +90,6 @@ public class AccountWebController {
} }
} }
} }
SAML2 saml2 = securityProps.getSaml2(); SAML2 saml2 = securityProps.getSaml2();
if (securityProps.isSaml2Activ() if (securityProps.isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) { && applicationProperties.getSystem().getEnableAlphaFunctionality()) {
@@ -98,16 +100,12 @@ public class AccountWebController {
.entrySet() .entrySet()
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null); .removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList); model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", securityProps.getLoginMethod()); model.addAttribute("loginMethod", securityProps.getLoginMethod());
boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false; boolean altLogin = providerList.size() > 0 ? securityProps.isAltLogin() : false;
model.addAttribute("altLogin", altLogin); model.addAttribute("altLogin", altLogin);
model.addAttribute("currentPage", "login"); model.addAttribute("currentPage", "login");
String error = request.getParameter("error"); String error = request.getParameter("error");
if (error != null) { if (error != null) {
switch (error) { switch (error) {
case "badcredentials": case "badcredentials":
error = "login.invalid"; error = "login.invalid";
@@ -121,12 +119,10 @@ public class AccountWebController {
default: default:
break; break;
} }
model.addAttribute("error", error); model.addAttribute("error", error);
} }
String erroroauth = request.getParameter("erroroauth"); String erroroauth = request.getParameter("erroroauth");
if (erroroauth != null) { if (erroroauth != null) {
switch (erroroauth) { switch (erroroauth) {
case "oauth2AutoCreateDisabled": case "oauth2AutoCreateDisabled":
erroroauth = "login.oauth2AutoCreateDisabled"; erroroauth = "login.oauth2AutoCreateDisabled";
@@ -167,8 +163,8 @@ public class AccountWebController {
case "invalid_destination": case "invalid_destination":
erroroauth = "login.invalid_destination"; erroroauth = "login.invalid_destination";
break; break;
// Valid InResponseTo was not available from the validation context, unable to // Valid InResponseTo was not available from the validation context, unable to
// evaluate // evaluate
case "invalid_in_response_to": case "invalid_in_response_to":
erroroauth = "login.invalid_in_response_to"; erroroauth = "login.invalid_in_response_to";
break; break;
@@ -178,18 +174,14 @@ public class AccountWebController {
default: default:
break; break;
} }
model.addAttribute("erroroauth", erroroauth); model.addAttribute("erroroauth", erroroauth);
} }
if (request.getParameter("messageType") != null) { if (request.getParameter("messageType") != null) {
model.addAttribute("messageType", "changedCredsMessage"); model.addAttribute("messageType", "changedCredsMessage");
} }
if (request.getParameter("logout") != null) { if (request.getParameter("logout") != null) {
model.addAttribute("logoutMessage", "You have been logged out."); model.addAttribute("logoutMessage", "You have been logged out.");
} }
return "login"; return "login";
} }
@@ -200,14 +192,11 @@ public class AccountWebController {
List<User> allUsers = userRepository.findAll(); List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status // Map to store session information and user activity status
Map<String, Boolean> userSessions = new HashMap<>(); Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>(); Map<String, Date> userLastRequest = new HashMap<>();
int activeUsers = 0; int activeUsers = 0;
int disabledUsers = 0; int disabledUsers = 0;
while (iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if (user != null) { if (user != null) {
@@ -215,22 +204,20 @@ public class AccountWebController {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove(); iterator.remove();
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId()); roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
break; // Break out of the inner loop once the user is removed // Break out of the inner loop once the user is removed
break;
} }
} }
// Determine the user's session status and last request time // Determine the user's session status and last request time
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false; boolean hasActiveSession = false;
Date lastRequest = null; Date lastRequest = null;
Optional<SessionEntity> latestSession = Optional<SessionEntity> latestSession =
sessionPersistentRegistry.findLatestSession(user.getUsername()); sessionPersistentRegistry.findLatestSession(user.getUsername());
if (latestSession.isPresent()) { if (latestSession.isPresent()) {
SessionEntity sessionEntity = latestSession.get(); SessionEntity sessionEntity = latestSession.get();
Date lastAccessedTime = sessionEntity.getLastRequest(); Date lastAccessedTime = sessionEntity.getLastRequest();
Instant now = Instant.now(); Instant now = Instant.now();
// Calculate session expiration and update session status accordingly // Calculate session expiration and update session status accordingly
Instant expirationTime = Instant expirationTime =
lastAccessedTime lastAccessedTime
@@ -242,16 +229,14 @@ public class AccountWebController {
} else { } else {
hasActiveSession = !sessionEntity.isExpired(); hasActiveSession = !sessionEntity.isExpired();
} }
lastRequest = sessionEntity.getLastRequest(); lastRequest = sessionEntity.getLastRequest();
} else { } else {
hasActiveSession = false; hasActiveSession = false;
lastRequest = new Date(0); // No session, set default last request time // No session, set default last request time
lastRequest = new Date(0);
} }
userSessions.put(user.getUsername(), hasActiveSession); userSessions.put(user.getUsername(), hasActiveSession);
userLastRequest.put(user.getUsername(), lastRequest); userLastRequest.put(user.getUsername(), lastRequest);
if (hasActiveSession) { if (hasActiveSession) {
activeUsers++; activeUsers++;
} }
@@ -260,7 +245,6 @@ public class AccountWebController {
} }
} }
} }
// Sort users by active status and last request date // Sort users by active status and last request date
List<User> sortedUsers = List<User> sortedUsers =
allUsers.stream() allUsers.stream()
@@ -268,7 +252,6 @@ public class AccountWebController {
(u1, u2) -> { (u1, u2) -> {
boolean u1Active = userSessions.get(u1.getUsername()); boolean u1Active = userSessions.get(u1.getUsername());
boolean u2Active = userSessions.get(u2.getUsername()); boolean u2Active = userSessions.get(u2.getUsername());
if (u1Active && !u2Active) { if (u1Active && !u2Active) {
return -1; return -1;
} else if (!u1Active && u2Active) { } else if (!u1Active && u2Active) {
@@ -284,9 +267,7 @@ public class AccountWebController {
} }
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
String deleteMessage = null; String deleteMessage = null;
if (messageType != null) { if (messageType != null) {
switch (messageType) { switch (messageType) {
@@ -300,7 +281,6 @@ public class AccountWebController {
break; break;
} }
model.addAttribute("deleteMessage", deleteMessage); model.addAttribute("deleteMessage", deleteMessage);
String addMessage = null; String addMessage = null;
switch (messageType) { switch (messageType) {
case "usernameExists": case "usernameExists":
@@ -317,7 +297,6 @@ public class AccountWebController {
} }
model.addAttribute("addMessage", addMessage); model.addAttribute("addMessage", addMessage);
} }
String changeMessage = null; String changeMessage = null;
if (messageType != null) { if (messageType != null) {
switch (messageType) { switch (messageType) {
@@ -336,7 +315,6 @@ public class AccountWebController {
} }
model.addAttribute("changeMessage", changeMessage); model.addAttribute("changeMessage", changeMessage);
} }
model.addAttribute("users", sortedUsers); model.addAttribute("users", sortedUsers);
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
model.addAttribute("roleDetails", roleDetails); model.addAttribute("roleDetails", roleDetails);
@@ -357,21 +335,17 @@ public class AccountWebController {
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String username = null; String username = null;
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails // Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal; UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes // Retrieve username and other attributes
username = userDetails.getUsername(); username = userDetails.getUsername();
// Add oAuth2 Login attributes to the model // Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", false); model.addAttribute("oAuth2Login", false);
} }
if (principal instanceof OAuth2User) { if (principal instanceof OAuth2User) {
// Cast the principal object to OAuth2User // Cast the principal object to OAuth2User
OAuth2User userDetails = (OAuth2User) principal; OAuth2User userDetails = (OAuth2User) principal;
// Retrieve username and other attributes // Retrieve username and other attributes
username = username =
userDetails.getAttribute( userDetails.getAttribute(
@@ -383,22 +357,21 @@ public class AccountWebController {
// Cast the principal object to OAuth2User // Cast the principal object to OAuth2User
CustomSaml2AuthenticatedPrincipal userDetails = CustomSaml2AuthenticatedPrincipal userDetails =
(CustomSaml2AuthenticatedPrincipal) principal; (CustomSaml2AuthenticatedPrincipal) principal;
// Retrieve username and other attributes // Retrieve username and other attributes
username = userDetails.getName(); username = userDetails.getName();
// Add oAuth2 Login attributes to the model // Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true); model.addAttribute("oAuth2Login", true);
} }
if (username != null) { if (username != null) {
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = Optional<User> user =
userRepository.findByUsernameIgnoreCaseWithSettings( userRepository
username); // Assuming findByUsername method exists .findByUsernameIgnoreCaseWithSettings( // Assuming findByUsername
// method exists
username);
if (!user.isPresent()) { if (!user.isPresent()) {
return "redirect:/error"; return "redirect:/error";
} }
// Convert settings map to JSON string // Convert settings map to JSON string
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
String settingsJson; String settingsJson;
@@ -409,7 +382,6 @@ public class AccountWebController {
log.error("exception", e); log.error("exception", e);
return "redirect:/error"; return "redirect:/error";
} }
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
if (messageType != null) { if (messageType != null) {
switch (messageType) { switch (messageType) {
@@ -433,7 +405,6 @@ public class AccountWebController {
} }
model.addAttribute("messageType", messageType); model.addAttribute("messageType", messageType);
} }
// Add attributes to the model // Add attributes to the model
model.addAttribute("username", username); model.addAttribute("username", username);
model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("role", user.get().getRolesAsString());
@@ -456,23 +427,21 @@ public class AccountWebController {
} }
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
// Cast the principal object to UserDetails // Cast the principal object to UserDetails
UserDetails userDetails = (UserDetails) principal; UserDetails userDetails = (UserDetails) principal;
// Retrieve username and other attributes // Retrieve username and other attributes
String username = userDetails.getUsername(); String username = userDetails.getUsername();
// Fetch user details from the database // Fetch user details from the database
Optional<User> user = Optional<User> user =
userRepository.findByUsernameIgnoreCase( userRepository
username); // Assuming findByUsername method exists .findByUsernameIgnoreCase( // Assuming findByUsername method exists
username);
if (!user.isPresent()) { if (!user.isPresent()) {
// Handle error appropriately // Handle error appropriately
return "redirect:/error"; // Example redirection in case of error // Example redirection in case of error
return "redirect:/error";
} }
String messageType = request.getParameter("messageType"); String messageType = request.getParameter("messageType");
if (messageType != null) { if (messageType != null) {
switch (messageType) { switch (messageType) {
@@ -493,7 +462,6 @@ public class AccountWebController {
} }
model.addAttribute("messageType", messageType); model.addAttribute("messageType", messageType);
} }
// Add attributes to the model // Add attributes to the model
model.addAttribute("username", username); model.addAttribute("username", username);
} }

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF.controller.web;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@@ -19,25 +18,25 @@ import stirling.software.SPDF.utils.FileInfo;
@Tag(name = "Database Management", description = "Database management and security APIs") @Tag(name = "Database Management", description = "Database management and security APIs")
public class DatabaseWebController { public class DatabaseWebController {
@Autowired private DatabaseBackupHelper databaseBackupHelper; private final DatabaseBackupHelper databaseBackupHelper;
public DatabaseWebController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseBackupHelper = databaseBackupHelper;
}
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/database") @GetMapping("/database")
public String database(HttpServletRequest request, Model model, Authentication authentication) { public String database(HttpServletRequest request, Model model, Authentication authentication) {
String error = request.getParameter("error"); String error = request.getParameter("error");
String confirmed = request.getParameter("infoMessage"); String confirmed = request.getParameter("infoMessage");
if (error != null) { if (error != null) {
model.addAttribute("error", error); model.addAttribute("error", error);
} else if (confirmed != null) { } else if (confirmed != null) {
model.addAttribute("infoMessage", confirmed); model.addAttribute("infoMessage", confirmed);
} }
List<FileInfo> backupList = databaseBackupHelper.getBackupList(); List<FileInfo> backupList = databaseBackupHelper.getBackupList();
model.addAttribute("backupFiles", backupList); model.addAttribute("backupFiles", backupList);
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version()); model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
return "database"; return "database";
} }
} }

View File

@@ -6,12 +6,7 @@ import java.nio.charset.StandardCharsets;
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.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -39,31 +34,41 @@ import stirling.software.SPDF.service.SignatureService;
@Slf4j @Slf4j
public class GeneralWebController { public class GeneralWebController {
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
private final SignatureService signatureService;
private final UserServiceInterface userService;
private final ResourceLoader resourceLoader;
public GeneralWebController(
SignatureService signatureService,
@Autowired(required = false) UserServiceInterface userService,
ResourceLoader resourceLoader) {
this.signatureService = signatureService;
this.userService = userService;
this.resourceLoader = resourceLoader;
}
@GetMapping("/pipeline") @GetMapping("/pipeline")
@Hidden @Hidden
public String pipelineForm(Model model) { public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline"); model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>(); List<String> pipelineConfigs = new ArrayList<>();
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>(); List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
if (new File("./pipeline/defaultWebUIConfigs/").exists()) { if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles = List<Path> jsonFiles =
paths.filter(Files::isRegularFile) paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json")) .filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList()); .collect(Collectors.toList());
for (Path jsonFile : jsonFiles) { for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8); String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content); pipelineConfigs.add(content);
} }
for (String config : pipelineConfigs) { for (String config : pipelineConfigs) {
Map<String, Object> jsonContent = Map<String, Object> jsonContent =
new ObjectMapper() new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {}); .readValue(config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name"); String name = (String) jsonContent.get("name");
if (name == null || name.length() < 1) { if (name == null || name.length() < 1) {
String filename = String filename =
@@ -78,7 +83,6 @@ public class GeneralWebController {
configWithName.put("name", name); configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName); pipelineConfigsWithNames.add(configWithName);
} }
} catch (IOException e) { } catch (IOException e) {
log.error("exception", e); log.error("exception", e);
} }
@@ -90,9 +94,7 @@ public class GeneralWebController {
pipelineConfigsWithNames.add(configWithName); pipelineConfigsWithNames.add(configWithName);
} }
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs); model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline"; return "pipeline";
} }
@@ -173,14 +175,6 @@ public class GeneralWebController {
return "split-pdfs"; return "split-pdfs";
} }
private static final String SIGNATURE_BASE_PATH = "customFiles/static/signatures/";
private static final String ALL_USERS_FOLDER = "ALL_USERS";
@Autowired private SignatureService signatureService;
@Autowired(required = false)
private UserServiceInterface userService;
@GetMapping("/sign") @GetMapping("/sign")
@Hidden @Hidden
public String signForm(Model model) { public String signForm(Model model) {
@@ -188,10 +182,8 @@ public class GeneralWebController {
if (userService != null) { if (userService != null) {
username = userService.getCurrentUsername(); username = userService.getCurrentUsername();
} }
// Get signatures from both personal and ALL_USERS folders // Get signatures from both personal and ALL_USERS folders
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username); List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
model.addAttribute("currentPage", "sign"); model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames()); model.addAttribute("fonts", getFontNames());
model.addAttribute("signatures", signatures); model.addAttribute("signatures", signatures);
@@ -226,17 +218,12 @@ public class GeneralWebController {
return "overlay-pdf"; return "overlay-pdf";
} }
@Autowired private ResourceLoader resourceLoader;
private List<FontResource> getFontNames() { private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>(); List<FontResource> fontNames = new ArrayList<>();
// Extract font names from classpath // Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2")); fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory // Extract font names from external directory
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*")); fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
return fontNames; return fontNames;
} }
@@ -283,13 +270,38 @@ public class GeneralWebController {
case "svg": case "svg":
return "svg"; return "svg";
default: default:
return ""; // or throw an exception if an unexpected extension is encountered // or throw an exception if an unexpected extension is encountered
return "";
} }
} }
@GetMapping("/crop")
@Hidden
public String cropForm(Model model) {
model.addAttribute("currentPage", "crop");
return "crop";
}
@GetMapping("/auto-split-pdf")
@Hidden
public String autoSPlitPDFForm(Model model) {
model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf";
}
@GetMapping("/remove-image-pdf")
@Hidden
public String removeImagePdfForm(Model model) {
model.addAttribute("currentPage", "remove-image-pdf");
return "remove-image-pdf";
}
public class FontResource { public class FontResource {
private String name; private String name;
private String extension; private String extension;
private String type; private String type;
public FontResource(String name, String extension) { public FontResource(String name, String extension) {
@@ -322,25 +334,4 @@ public class GeneralWebController {
this.type = type; this.type = type;
} }
} }
@GetMapping("/crop")
@Hidden
public String cropForm(Model model) {
model.addAttribute("currentPage", "crop");
return "crop";
}
@GetMapping("/auto-split-pdf")
@Hidden
public String autoSPlitPDFForm(Model model) {
model.addAttribute("currentPage", "auto-split-pdf");
return "auto-split-pdf";
}
@GetMapping("/remove-image-pdf")
@Hidden
public String removeImagePdfForm(Model model) {
model.addAttribute("currentPage", "remove-image-pdf");
return "remove-image-pdf";
}
} }

View File

@@ -6,7 +6,6 @@ import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -28,6 +27,12 @@ import stirling.software.SPDF.model.Dependency;
@Slf4j @Slf4j
public class HomeWebController { public class HomeWebController {
private final ApplicationProperties applicationProperties;
public HomeWebController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@GetMapping("/about") @GetMapping("/about")
@Hidden @Hidden
public String gameForm(Model model) { public String gameForm(Model model) {
@@ -69,8 +74,6 @@ 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

View File

@@ -5,7 +5,6 @@ import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -30,12 +29,18 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class MetricsController { public class MetricsController {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private boolean metricsEnabled; private boolean metricsEnabled;
public MetricsController(
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) {
this.applicationProperties = applicationProperties;
this.meterRegistry = meterRegistry;
}
@PostConstruct @PostConstruct
public void init() { public void init() {
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
@@ -43,11 +48,6 @@ public class MetricsController {
this.metricsEnabled = metricsEnabled; this.metricsEnabled = metricsEnabled;
} }
@Autowired
public MetricsController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@GetMapping("/status") @GetMapping("/status")
@Operation( @Operation(
summary = "Application status and version", summary = "Application status and version",
@@ -57,7 +57,6 @@ public class MetricsController {
if (!metricsEnabled) { if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
} }
Map<String, String> status = new HashMap<>(); Map<String, String> status = new HashMap<>();
status.put("status", "UP"); status.put("status", "UP");
status.put("version", getClass().getPackage().getImplementationVersion()); status.put("version", getClass().getPackage().getImplementationVersion());
@@ -236,7 +235,6 @@ public class MetricsController {
String uri = counter.getId().getTag("uri"); String uri = counter.getId().getTag("uri");
counts.merge(uri, counter.count(), Double::sum); counts.merge(uri, counter.count(), Double::sum);
}); });
List<EndpointCount> result = List<EndpointCount> result =
counts.entrySet().stream() counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
@@ -271,7 +269,6 @@ public class MetricsController {
private List<EndpointCount> getUniqueUserCounts(String method) { private List<EndpointCount> getUniqueUserCounts(String method) {
log.info("Getting unique user counts for method: {}", method); log.info("Getting unique user counts for method: {}", method);
Map<String, Set<String>> uniqueUsers = new HashMap<>(); Map<String, Set<String>> uniqueUsers = new HashMap<>();
meterRegistry meterRegistry
.find("http.requests") .find("http.requests")
.tag("method", method) .tag("method", method)
@@ -284,19 +281,37 @@ public class MetricsController {
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session); uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
} }
}); });
List<EndpointCount> result = List<EndpointCount> result =
uniqueUsers.entrySet().stream() uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size())) .map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed()) .sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList()); .collect(Collectors.toList());
log.info("Found {} endpoints with unique user counts", result.size()); log.info("Found {} endpoints with unique user counts", result.size());
return result; return result;
} }
@GetMapping("/uptime")
public ResponseEntity<?> getUptime() {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
LocalDateTime now = LocalDateTime.now();
Duration uptime = Duration.between(StartupApplicationListener.startTime, now);
return ResponseEntity.ok(formatDuration(uptime));
}
private String formatDuration(Duration duration) {
long days = duration.toDays();
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds);
}
public static class EndpointCount { public static class EndpointCount {
private String endpoint; private String endpoint;
private double count; private double count;
public EndpointCount(String endpoint, double count) { public EndpointCount(String endpoint, double count) {
@@ -320,23 +335,4 @@ public class MetricsController {
this.count = count; this.count = count;
} }
} }
@GetMapping("/uptime")
public ResponseEntity<?> getUptime() {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
LocalDateTime now = LocalDateTime.now();
Duration uptime = Duration.between(StartupApplicationListener.startTime, now);
return ResponseEntity.ok(formatDuration(uptime));
}
private String formatDuration(Duration duration) {
long days = duration.toDays();
long hours = duration.toHoursPart();
long minutes = duration.toMinutesPart();
long seconds = duration.toSecondsPart();
return String.format("%dd %dh %dm %ds", days, hours, minutes, seconds);
}
} }

View File

@@ -6,7 +6,6 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -22,7 +21,11 @@ import stirling.software.SPDF.utils.CheckProgramInstall;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class OtherWebController { public class OtherWebController {
@Autowired ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
public OtherWebController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@GetMapping("/compress-pdf") @GetMapping("/compress-pdf")
@Hidden @Hidden

View File

@@ -18,10 +18,16 @@ import stirling.software.SPDF.service.SignatureService;
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
public class SignatureController { public class SignatureController {
@Autowired private SignatureService signatureService; private final SignatureService signatureService;
@Autowired(required = false) private final UserServiceInterface userService;
private UserServiceInterface userService;
public SignatureController(
SignatureService signatureService,
@Autowired(required = false) UserServiceInterface userService) {
this.signatureService = signatureService;
this.userService = userService;
}
@GetMapping("/sign/{fileName}") @GetMapping("/sign/{fileName}")
public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName) public ResponseEntity<byte[]> getSignature(@PathVariable(name = "fileName") String fileName)
@@ -30,15 +36,14 @@ public class SignatureController {
if (userService != null) { if (userService != null) {
username = userService.getCurrentUsername(); username = userService.getCurrentUsername();
} }
// Verify access permission // Verify access permission
if (!signatureService.hasAccessToFile(username, fileName)) { if (!signatureService.hasAccessToFile(username, fileName)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} }
byte[] imageBytes = signatureService.getSignatureBytes(username, fileName); byte[] imageBytes = signatureService.getSignatureBytes(username, fileName);
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG) // Adjust based on file type .contentType( // Adjust based on file type
MediaType.IMAGE_JPEG)
.body(imageBytes); .body(imageBytes);
} }
} }

View File

@@ -79,6 +79,23 @@ public class ApplicationProperties {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
} }
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public enum LoginMethods { public enum LoginMethods {
ALL("all"), ALL("all"),
NORMAL("normal"), NORMAL("normal"),
@@ -97,23 +114,6 @@ public class ApplicationProperties {
} }
} }
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
@Data @Data
public static class InitialLogin { public static class InitialLogin {
private String username; private String username;
@@ -122,18 +122,19 @@ public class ApplicationProperties {
@Getter @Getter
@Setter @Setter
@ToString
public static class SAML2 { public static class SAML2 {
private Boolean enabled = false; private Boolean enabled = false;
private Boolean autoCreateUser = false; private Boolean autoCreateUser = false;
private Boolean blockRegistration = false; private Boolean blockRegistration = false;
private String registrationId = "stirling"; private String registrationId = "stirling";
private String idpMetadataUri; @ToString.Exclude private String idpMetadataUri;
private String idpSingleLogoutUrl; private String idpSingleLogoutUrl;
private String idpSingleLoginUrl; private String idpSingleLoginUrl;
private String idpIssuer; private String idpIssuer;
private String idpCert; private String idpCert;
private String privateKey; @ToString.Exclude private String privateKey;
private String spCert; @ToString.Exclude private String spCert;
public InputStream getIdpMetadataUri() throws IOException { public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) { if (idpMetadataUri.startsWith("classpath:")) {

View File

@@ -2,14 +2,7 @@ package stirling.software.SPDF.model;
import java.io.Serializable; import java.io.Serializable;
import jakarta.persistence.Column; import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity @Entity
@Table(name = "authorities") @Table(name = "authorities")
@@ -17,14 +10,6 @@ public class Authority implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
public Authority() {}
public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
user.getAuthorities().add(this);
}
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@@ -36,6 +21,14 @@ public class Authority implements Serializable {
@JoinColumn(name = "user_id") @JoinColumn(name = "user_id")
private User user; private User user;
public Authority() {}
public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
user.getAuthorities().add(this);
}
public Long getId() { public Long getId() {
return id; return id;
} }

View File

@@ -40,22 +40,6 @@ public enum Role {
this.roleName = roleName; this.roleName = roleName;
} }
public String getRoleId() {
return roleId;
}
public int getApiCallsPerDay() {
return apiCallsPerDay;
}
public int getWebCallsPerDay() {
return webCallsPerDay;
}
public String getRoleName() {
return roleName;
}
public static String getRoleNameByRoleId(String roleId) { public static String getRoleNameByRoleId(String roleId) {
// Using the fromString method to get the Role enum based on the roleId // Using the fromString method to get the Role enum based on the roleId
Role role = fromString(roleId); Role role = fromString(roleId);
@@ -81,4 +65,20 @@ public enum Role {
} }
throw new IllegalArgumentException("No Role defined for id: " + roleId); throw new IllegalArgumentException("No Role defined for id: " + roleId);
} }
public String getRoleId() {
return roleId;
}
public int getApiCallsPerDay() {
return apiCallsPerDay;
}
public int getWebCallsPerDay() {
return webCallsPerDay;
}
public String getRoleName() {
return roleName;
}
} }

View File

@@ -111,14 +111,14 @@ public class User implements Serializable {
this.enabled = enabled; this.enabled = enabled;
} }
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
}
public String getAuthenticationType() { public String getAuthenticationType() {
return authenticationType; return authenticationType;
} }
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
}
public Set<Authority> getAuthorities() { public Set<Authority> getAuthorities() {
return authorities; return authorities;
} }

View File

@@ -12,6 +12,10 @@ public class GithubProvider extends Provider {
private static final String authorizationUri = "https://github.com/login/oauth/authorize"; private static final String authorizationUri = "https://github.com/login/oauth/authorize";
private static final String tokenUri = "https://github.com/login/oauth/access_token"; private static final String tokenUri = "https://github.com/login/oauth/access_token";
private static final String userInfoUri = "https://api.github.com/user"; private static final String userInfoUri = "https://api.github.com/user";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
public String getAuthorizationuri() { public String getAuthorizationuri() {
return authorizationUri; return authorizationUri;
@@ -25,11 +29,6 @@ public class GithubProvider extends Provider {
return userInfoUri; return userInfoUri;
} }
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
@Override @Override
public String getIssuer() { public String getIssuer() {
return new String(); return new String();

View File

@@ -13,6 +13,10 @@ public class GoogleProvider extends Provider {
private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token"; private static final String tokenUri = "https://www.googleapis.com/oauth2/v4/token";
private static final String userInfoUri = private static final String userInfoUri =
"https://www.googleapis.com/oauth2/v3/userinfo?alt=json"; "https://www.googleapis.com/oauth2/v3/userinfo?alt=json";
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
public String getAuthorizationuri() { public String getAuthorizationuri() {
return authorizationUri; return authorizationUri;
@@ -26,11 +30,6 @@ public class GoogleProvider extends Provider {
return userInfoUri; return userInfoUri;
} }
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "email";
@Override @Override
public String getIssuer() { public String getIssuer() {
return new String(); return new String();

View File

@@ -21,16 +21,6 @@ public class TextFinder extends PDFTextStripper {
private final boolean wholeWordSearch; private final boolean wholeWordSearch;
private final List<PDFText> textOccurrences = new ArrayList<>(); private final List<PDFText> textOccurrences = new ArrayList<>();
private class MatchInfo {
int startIndex;
int matchLength;
MatchInfo(int startIndex, int matchLength) {
this.startIndex = startIndex;
this.matchLength = matchLength;
}
}
public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch)
throws IOException { throws IOException {
this.searchText = searchText.toLowerCase(); this.searchText = searchText.toLowerCase();
@@ -103,4 +93,14 @@ public class TextFinder extends PDFTextStripper {
return textOccurrences; return textOccurrences;
} }
private class MatchInfo {
int startIndex;
int matchLength;
MatchInfo(int startIndex, int matchLength) {
this.startIndex = startIndex;
this.matchLength = matchLength;
}
}
} }

View File

@@ -2,7 +2,6 @@ package stirling.software.SPDF.repository;
import java.util.Date; import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -11,7 +10,11 @@ import stirling.software.SPDF.model.PersistentLogin;
public class JPATokenRepositoryImpl implements PersistentTokenRepository { public class JPATokenRepositoryImpl implements PersistentTokenRepository {
@Autowired private PersistentLoginRepository persistentLoginRepository; private final PersistentLoginRepository persistentLoginRepository;
public JPATokenRepositoryImpl(PersistentLoginRepository persistentLoginRepository) {
this.persistentLoginRepository = persistentLoginRepository;
}
@Override @Override
@Transactional @Transactional

View File

@@ -1,25 +1,10 @@
package stirling.software.SPDF.service; package stirling.software.SPDF.service;
import java.io.BufferedReader; import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.KeyStoreException; import java.security.KeyStoreException;
import java.security.cert.CertPath; import java.security.cert.*;
import java.security.cert.CertPathValidator; import java.util.*;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -6,11 +6,7 @@ import java.net.InetAddress;
import java.net.NetworkInterface; import java.net.NetworkInterface;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.Enumeration; import java.util.*;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -31,11 +27,13 @@ public class PostHogService {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final Environment env; private final Environment env;
private boolean configDirMounted;
@Autowired @Autowired
public PostHogService( public PostHogService(
PostHog postHog, PostHog postHog,
@Qualifier("UUID") String uuid, @Qualifier("UUID") String uuid,
@Qualifier("configDirMounted") boolean configDirMounted,
@Qualifier("appVersion") String appVersion, @Qualifier("appVersion") String appVersion,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Autowired(required = false) UserServiceInterface userService, @Autowired(required = false) UserServiceInterface userService,
@@ -46,6 +44,7 @@ public class PostHogService {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.userService = userService; this.userService = userService;
this.env = env; this.env = env;
this.configDirMounted = configDirMounted;
captureSystemInfo(); captureSystemInfo();
} }
@@ -80,6 +79,7 @@ public class PostHogService {
deploymentType = "DOCKER"; deploymentType = "DOCKER";
} }
metrics.put("deployment_type", deploymentType); metrics.put("deployment_type", deploymentType);
metrics.put("mounted_config_dir", configDirMounted);
// System info // System info
metrics.put("os_name", System.getProperty("os.name")); metrics.put("os_name", System.getProperty("os.name"));

View File

@@ -11,15 +11,14 @@ import lombok.Data;
@AllArgsConstructor @AllArgsConstructor
@Data @Data
public class FileInfo { public class FileInfo {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String fileName; private String fileName;
private String filePath; private String filePath;
private LocalDateTime modificationDate; private LocalDateTime modificationDate;
private long fileSize; private long fileSize;
private LocalDateTime creationDate; private LocalDateTime creationDate;
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// Converts the file path string to a Path object. // Converts the file path string to a Path object.
public Path getFilePathAsPath() { public Path getFilePathAsPath() {
return Paths.get(filePath); return Paths.get(filePath);

View File

@@ -1,11 +1,6 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult; import java.nio.file.FileVisitResult;
import java.nio.file.Files; import java.nio.file.Files;

View File

@@ -4,18 +4,9 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection; import java.net.*;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult; import java.nio.file.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;

View File

@@ -1,11 +1,7 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.awt.geom.AffineTransform; import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp; import java.awt.image.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;

View File

@@ -126,82 +126,6 @@ public class PdfUtils {
return pageText.contains(phrase); return pageText.contains(phrase);
} }
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
String pdfText = "";
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
pdfText = textStripper.getText(pdfDocument);
} else {
// remove whitespaces
pagesToCheck = pagesToCheck.replaceAll("\\s+", "");
String[] splitPoints = pagesToCheck.split(",");
for (String splitPoint : splitPoints) {
if (splitPoint.contains("-")) {
// Handle page ranges
String[] range = splitPoint.split("-");
int startPage = Integer.parseInt(range[0]);
int endPage = Integer.parseInt(range[1]);
for (int i = startPage; i <= endPage; i++) {
textStripper.setStartPage(i);
textStripper.setEndPage(i);
pdfText += textStripper.getText(pdfDocument);
}
} else {
// Handle individual page
int page = Integer.parseInt(splitPoint);
textStripper.setStartPage(page);
textStripper.setEndPage(page);
pdfText += textStripper.getText(pdfDocument);
}
}
}
pdfDocument.close();
return pdfText.contains(text);
}
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
throws IOException {
int actualPageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
switch (comparator.toLowerCase()) {
case "greater":
return actualPageCount > pageCount;
case "equal":
return actualPageCount == pageCount;
case "less":
return actualPageCount < pageCount;
default:
throw new IllegalArgumentException(
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
}
}
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
PDPage firstPage = pdfDocument.getPage(0);
PDRectangle mediaBox = firstPage.getMediaBox();
float actualPageWidth = mediaBox.getWidth();
float actualPageHeight = mediaBox.getHeight();
pdfDocument.close();
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842"
// for A4
String[] dimensions = expectedPageSize.split("x");
float expectedPageWidth = Float.parseFloat(dimensions[0]);
float expectedPageHeight = Float.parseFloat(dimensions[1]);
// Checks if the actual page size matches the expected page size
return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight;
}
public static byte[] convertFromPdf( public static byte[] convertFromPdf(
byte[] inputStream, byte[] inputStream,
String imageType, String imageType,
@@ -352,12 +276,17 @@ public class PdfUtils {
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) { for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB);
PDPage newPage = new PDPage(new PDRectangle(bim.getWidth(), bim.getHeight())); PDPage originalPage = document.getPage(page);
float width = originalPage.getMediaBox().getWidth();
float height = originalPage.getMediaBox().getHeight();
PDPage newPage = new PDPage(new PDRectangle(width, height));
imageDocument.addPage(newPage); imageDocument.addPage(newPage);
PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim); PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
PDPageContentStream contentStream = PDPageContentStream contentStream =
new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true); new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
contentStream.drawImage(pdImage, 0, 0); contentStream.drawImage(pdImage, 0, 0, width, height);
contentStream.close(); contentStream.close();
} }
return imageDocument; return imageDocument;
@@ -513,6 +442,82 @@ public class PdfUtils {
return baos.toByteArray(); return baos.toByteArray();
} }
public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck)
throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
String pdfText = "";
if (pagesToCheck == null || "all".equals(pagesToCheck)) {
pdfText = textStripper.getText(pdfDocument);
} else {
// remove whitespaces
pagesToCheck = pagesToCheck.replaceAll("\\s+", "");
String[] splitPoints = pagesToCheck.split(",");
for (String splitPoint : splitPoints) {
if (splitPoint.contains("-")) {
// Handle page ranges
String[] range = splitPoint.split("-");
int startPage = Integer.parseInt(range[0]);
int endPage = Integer.parseInt(range[1]);
for (int i = startPage; i <= endPage; i++) {
textStripper.setStartPage(i);
textStripper.setEndPage(i);
pdfText += textStripper.getText(pdfDocument);
}
} else {
// Handle individual page
int page = Integer.parseInt(splitPoint);
textStripper.setStartPage(page);
textStripper.setEndPage(page);
pdfText += textStripper.getText(pdfDocument);
}
}
}
pdfDocument.close();
return pdfText.contains(text);
}
public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator)
throws IOException {
int actualPageCount = pdfDocument.getNumberOfPages();
pdfDocument.close();
switch (comparator.toLowerCase()) {
case "greater":
return actualPageCount > pageCount;
case "equal":
return actualPageCount == pageCount;
case "less":
return actualPageCount < pageCount;
default:
throw new IllegalArgumentException(
"Invalid comparator. Only 'greater', 'equal', and 'less' are supported.");
}
}
public boolean pageSize(PDDocument pdfDocument, String expectedPageSize) throws IOException {
PDPage firstPage = pdfDocument.getPage(0);
PDRectangle mediaBox = firstPage.getMediaBox();
float actualPageWidth = mediaBox.getWidth();
float actualPageHeight = mediaBox.getHeight();
pdfDocument.close();
// Assumes the expectedPageSize is in the format "widthxheight", e.g. "595x842"
// for A4
String[] dimensions = expectedPageSize.split("x");
float expectedPageWidth = Float.parseFloat(dimensions[0]);
float expectedPageHeight = Float.parseFloat(dimensions[1]);
// Checks if the actual page size matches the expected page size
return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight;
}
/** Key for storing the dimensions of a rendered image in a map. */ /** Key for storing the dimensions of a rendered image in a map. */
private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {} private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {}

View File

@@ -1,10 +1,6 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import java.io.BufferedReader; import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -21,20 +17,17 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class ProcessExecutor { public class ProcessExecutor {
private static ApplicationProperties applicationProperties = new ApplicationProperties();
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
PYTHON_OPENCV,
WEASYPRINT,
INSTALL_APP,
CALIBRE,
TESSERACT,
QPDF
}
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>(); private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
private static ApplicationProperties applicationProperties = new ApplicationProperties();
private final Semaphore semaphore;
private final boolean liveUpdates;
private long timeoutDuration;
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
this.semaphore = new Semaphore(semaphoreLimit);
this.liveUpdates = liveUpdates;
this.timeoutDuration = timeout;
}
public static ProcessExecutor getInstance(Processes processType) { public static ProcessExecutor getInstance(Processes processType) {
return getInstance(processType, true); return getInstance(processType, true);
@@ -135,16 +128,6 @@ public class ProcessExecutor {
}); });
} }
private final Semaphore semaphore;
private final boolean liveUpdates;
private long timeoutDuration;
private ProcessExecutor(int semaphoreLimit, boolean liveUpdates, long timeout) {
this.semaphore = new Semaphore(semaphoreLimit);
this.liveUpdates = liveUpdates;
this.timeoutDuration = timeout;
}
public ProcessExecutorResult runCommandWithOutputHandling(List<String> command) public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
throws IOException, InterruptedException { throws IOException, InterruptedException {
return runCommandWithOutputHandling(command, null); return runCommandWithOutputHandling(command, null);
@@ -271,6 +254,17 @@ public class ProcessExecutor {
return new ProcessExecutorResult(exitCode, messages); return new ProcessExecutorResult(exitCode, messages);
} }
public enum Processes {
LIBRE_OFFICE,
PDFTOHTML,
PYTHON_OPENCV,
WEASYPRINT,
INSTALL_APP,
CALIBRE,
TESSERACT,
QPDF
}
public class ProcessExecutorResult { public class ProcessExecutorResult {
int rc; int rc;
String messages; String messages;

View File

@@ -14,7 +14,10 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree; import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.font.*; import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDFontFactory;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.text.TextPosition;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;

View File

@@ -1,5 +1,4 @@
multipart.enabled=true multipart.enabled=true
logging.level.org.springframework=WARN logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN logging.level.org.hibernate=WARN
logging.level.org.eclipse.jetty=WARN logging.level.org.eclipse.jetty=WARN
@@ -8,52 +7,38 @@ logging.level.org.eclipse.jetty=WARN
#logging.level.org.opensaml=DEBUG #logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.SPDF.config.security: DEBUG #logging.level.stirling.software.SPDF.config.security: DEBUG
logging.level.com.zaxxer.hikari=WARN logging.level.com.zaxxer.hikari=WARN
spring.jpa.open-in-view=false spring.jpa.open-in-view=false
server.forward-headers-strategy=NATIVE server.forward-headers-strategy=NATIVE
server.error.path=/error server.error.path=/error
server.error.whitelabel.enabled=false server.error.whitelabel.enabled=false
server.error.include-stacktrace=always server.error.include-stacktrace=always
server.error.include-exception=true server.error.include-exception=true
server.error.include-message=always server.error.include-message=always
#logging.level.org.springframework.web=DEBUG #logging.level.org.springframework.web=DEBUG
#logging.level.org.springframework=DEBUG #logging.level.org.springframework=DEBUG
#logging.level.org.springframework.security=DEBUG #logging.level.org.springframework.security=DEBUG
spring.servlet.multipart.max-file-size=2000MB spring.servlet.multipart.max-file-size=2000MB
spring.servlet.multipart.max-request-size=2000MB spring.servlet.multipart.max-request-size=2000MB
server.servlet.session.tracking-modes=cookie server.servlet.session.tracking-modes=cookie
server.servlet.context-path=${SYSTEM_ROOTURIPATH:/} server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
spring.devtools.restart.enabled=true spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/** spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
#spring.thymeleaf.cache=false #spring.thymeleaf.cache=false
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=
spring.h2.console.enabled=false spring.h2.console.enabled=false
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
server.servlet.session.timeout: 30m server.servlet.session.timeout:30m
# Change the default URL path for OpenAPI JSON # Change the default URL path for OpenAPI JSON
springdoc.api-docs.path=/v1/api-docs springdoc.api-docs.path=/v1/api-docs
# Set the URL of the OpenAPI JSON for the Swagger UI # Set the URL of the OpenAPI JSON for the Swagger UI
springdoc.swagger-ui.url=/v1/api-docs springdoc.swagger-ui.url=/v1/api-docs
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
posthog.host=https://eu.i.posthog.com posthog.host=https://eu.i.posthog.com

View File

@@ -11,8 +11,8 @@
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/invalid-auths.log</file> <file>logs/invalid-auths.log</file>
<encoder> <encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern> <pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder> </encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover and keep 7 days' worth of history --> <!-- daily rollover and keep 7 days' worth of history -->
@@ -21,12 +21,12 @@
</rollingPolicy> </rollingPolicy>
</appender> </appender>
<!-- Rolling File Appender --> <!-- Rolling File Appender -->
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender"> <appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/info.log</file> <file>logs/info.log</file>
<encoder> <encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern> <pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder> </encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover and keep 7 days' worth of history --> <!-- daily rollover and keep 7 days' worth of history -->
@@ -43,7 +43,8 @@
</root> </root>
<!-- Specific Logger --> <!-- Specific Logger -->
<logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" level="ERROR" additivity="false"> <logger name="stirling.software.SPDF.config.security.CustomAuthenticationFailureHandler" level="ERROR"
additivity="false">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>
<appender-ref ref="AUTHLOG"/> <appender-ref ref="AUTHLOG"/>
</logger> </logger>

View File

@@ -238,11 +238,13 @@ database.creationDate=تاريخ الإنشاء
database.fileSize=حجم الملف database.fileSize=حجم الملف
database.deleteBackupFile=حذف ملف النسخ الاحتياطي database.deleteBackupFile=حذف ملف النسخ الاحتياطي
database.importBackupFile=استيراد ملف النسخ الاحتياطي database.importBackupFile=استيراد ملف النسخ الاحتياطي
database.createBackupFile=Create Backup File
database.downloadBackupFile=تنزيل ملف النسخ الاحتياطي database.downloadBackupFile=تنزيل ملف النسخ الاحتياطي
database.info_1=عند استيراد البيانات، من الضروري ضمان الهيكل الصحيح. إذا كنت غير متأكد مما تفعله، اطلب المشورة والدعم من محترف. يمكن أن يؤدي الخطأ في الهيكل إلى حدوث أعطال في التطبيق، حتى عدم القدرة على تشغيل التطبيق بالكامل. database.info_1=عند استيراد البيانات، من الضروري ضمان الهيكل الصحيح. إذا كنت غير متأكد مما تفعله، اطلب المشورة والدعم من محترف. يمكن أن يؤدي الخطأ في الهيكل إلى حدوث أعطال في التطبيق، حتى عدم القدرة على تشغيل التطبيق بالكامل.
database.info_2=لا يهم اسم الملف عند التحميل. سيتم إعادة تسميته بعد ذلك لاتباع التنسيق backup_user_yyyyMMddHHmm.sql، مما يضمن اتساق تسمية متناسق. database.info_2=لا يهم اسم الملف عند التحميل. سيتم إعادة تسميته بعد ذلك لاتباع التنسيق backup_user_yyyyMMddHHmm.sql، مما يضمن اتساق تسمية متناسق.
database.submit=استيراد النسخة الاحتياطية database.submit=استيراد النسخة الاحتياطية
database.importIntoDatabaseSuccessed=تم استيراد قاعدة البيانات بنجاح database.importIntoDatabaseSuccessed=تم استيراد قاعدة البيانات بنجاح
database.backupCreated=Database backup successful
database.fileNotFound=لم يتم العثور على الملف database.fileNotFound=لم يتم العثور على الملف
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
database.failedImportFile=فشل استيراد الملف database.failedImportFile=فشل استيراد الملف

View File

@@ -238,11 +238,13 @@ database.creationDate=Yaradılma tarixi
database.fileSize=Fayl Ölçüsü database.fileSize=Fayl Ölçüsü
database.deleteBackupFile=Yedək Faylını Sil database.deleteBackupFile=Yedək Faylını Sil
database.importBackupFile=Yedək Faylını Daxil Et database.importBackupFile=Yedək Faylını Daxil Et
database.createBackupFile=Create Backup File
database.downloadBackupFile=Yedək Faylını Yüklə database.downloadBackupFile=Yedək Faylını Yüklə
database.info_1=Məlumatı daxil edərkən doğru strukturun mövcudluğundan əmin olmaq vacibdir. Əgər nə etdiyinizdən əmin deyilsinizsə, professional birindən məsləhət və yardım alın. Strukturdakı xəta proqramdakı nasazlıqlardan proqramı çalışdırma qabiliyyətinin tamamilə aradan qalxmasına qədər bir sıra problemlərə səbəb ola bilər. database.info_1=Məlumatı daxil edərkən doğru strukturun mövcudluğundan əmin olmaq vacibdir. Əgər nə etdiyinizdən əmin deyilsinizsə, professional birindən məsləhət və yardım alın. Strukturdakı xəta proqramdakı nasazlıqlardan proqramı çalışdırma qabiliyyətinin tamamilə aradan qalxmasına qədər bir sıra problemlərə səbəb ola bilər.
database.info_2=Faylın adı fayl yüklənərkən önəmli deyildir. Faylın adı sonradan sabit adlandırmanın varlığından əmin olmaq məqsədilə backup_user_yyyyMMddHHmm.sql tərzində formata dəyişdiriləcəkdir. database.info_2=Faylın adı fayl yüklənərkən önəmli deyildir. Faylın adı sonradan sabit adlandırmanın varlığından əmin olmaq məqsədilə backup_user_yyyyMMddHHmm.sql tərzində formata dəyişdiriləcəkdir.
database.submit=Yedəkləməni Daxil Et database.submit=Yedəkləməni Daxil Et
database.importIntoDatabaseSuccessed=Verilənlər bazasına daxil etmə uğurla nəticələndi database.importIntoDatabaseSuccessed=Verilənlər bazasına daxil etmə uğurla nəticələndi
database.backupCreated=Database backup successful
database.fileNotFound=Fayl Tapılmadı database.fileNotFound=Fayl Tapılmadı
database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır database.fileNullOrEmpty=Fayl boş və ya "null" olmamalıdır
database.failedImportFile=Faylı daxil etmək alınmadı database.failedImportFile=Faylı daxil etmək alınmadı

View File

@@ -238,11 +238,13 @@ database.creationDate=Дата на създаване
database.fileSize=Размер на файла database.fileSize=Размер на файла
database.deleteBackupFile=Изтриване на архивен файл database.deleteBackupFile=Изтриване на архивен файл
database.importBackupFile=Импортиране на архивен файл database.importBackupFile=Импортиране на архивен файл
database.createBackupFile=Create Backup File
database.downloadBackupFile=Изтеглете архивен файл database.downloadBackupFile=Изтеглете архивен файл
database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението. database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението.
database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване. database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване.
database.submit=Импортиране на резервно копие database.submit=Импортиране на резервно копие
database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно
database.backupCreated=Database backup successful
database.fileNotFound=Файлът не е намерен database.fileNotFound=Файлът не е намерен
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
database.failedImportFile=Неуспешно импортиране на файл database.failedImportFile=Неуспешно импортиране на файл

View File

@@ -238,11 +238,13 @@ database.creationDate=Data de Creació
database.fileSize=Mida del Fitxer database.fileSize=Mida del Fitxer
database.deleteBackupFile=Elimina el Fitxer de Còpia de Seguretat database.deleteBackupFile=Elimina el Fitxer de Còpia de Seguretat
database.importBackupFile=Importa el Fitxer de Còpia de Seguretat database.importBackupFile=Importa el Fitxer de Còpia de Seguretat
database.createBackupFile=Create Backup File
database.downloadBackupFile=Descarrega el Fitxer de Còpia de Seguretat database.downloadBackupFile=Descarrega el Fitxer de Còpia de Seguretat
database.info_1=Quan importis dades, és crucial assegurar-se que l'estructura sigui correcta. Si no estàs segur del que fas, busca l'assessorament d'un professional. Un error en l'estructura pot causar malfuncionaments de l'aplicació, fins i tot impossibilitar-ne l'execució. database.info_1=Quan importis dades, és crucial assegurar-se que l'estructura sigui correcta. Si no estàs segur del que fas, busca l'assessorament d'un professional. Un error en l'estructura pot causar malfuncionaments de l'aplicació, fins i tot impossibilitar-ne l'execució.
database.info_2=El nom del fitxer no importa quan es puja. Es renombrarà després per seguir el format backup_user_yyyyMMddHHmm.sql, assegurant una convenció de nomenclatura consistent. database.info_2=El nom del fitxer no importa quan es puja. Es renombrarà després per seguir el format backup_user_yyyyMMddHHmm.sql, assegurant una convenció de nomenclatura consistent.
database.submit=Importa la Còpia de Seguretat database.submit=Importa la Còpia de Seguretat
database.importIntoDatabaseSuccessed=Importació a la base de dades completada amb èxit database.importIntoDatabaseSuccessed=Importació a la base de dades completada amb èxit
database.backupCreated=Database backup successful
database.fileNotFound=Fitxer no trobat database.fileNotFound=Fitxer no trobat
database.fileNullOrEmpty=El fitxer no ha de ser nul o buit database.fileNullOrEmpty=El fitxer no ha de ser nul o buit
database.failedImportFile=Error en la importació del fitxer database.failedImportFile=Error en la importació del fitxer

View File

@@ -238,11 +238,13 @@ database.creationDate=Datum vytvoření
database.fileSize=Velikost souboru database.fileSize=Velikost souboru
database.deleteBackupFile=Smazat záložní soubor database.deleteBackupFile=Smazat záložní soubor
database.importBackupFile=Import zálohy souboru database.importBackupFile=Import zálohy souboru
database.createBackupFile=Create Backup File
database.downloadBackupFile=Stáhnout zálohový soubor database.downloadBackupFile=Stáhnout zálohový soubor
database.info_1=Při importu dat je důležité zajistit správnou strukturu. Pokud jste nejistí, jak se chovat, hledajte konzultaci a podporu od profesionala. Chyba v struktuře může vést k selhání aplikace, dokonce i k tomu, že by aplikace nemohla být spuštěna. database.info_1=Při importu dat je důležité zajistit správnou strukturu. Pokud jste nejistí, jak se chovat, hledajte konzultaci a podporu od profesionala. Chyba v struktuře může vést k selhání aplikace, dokonce i k tomu, že by aplikace nemohla být spuštěna.
database.info_2=Název souboru nezáleží při nahrávání. Bude jeho zpětně znovu pojmenován podle formáту backup_user_yyyyMMddHHmm.sql, což zajišťuje konzistentní pravidlo označení. database.info_2=Název souboru nezáleží při nahrávání. Bude jeho zpětně znovu pojmenován podle formáту backup_user_yyyyMMddHHmm.sql, což zajišťuje konzistentní pravidlo označení.
database.submit=Import zálohy database.submit=Import zálohy
database.importIntoDatabaseSuccessed=Import do databáze byl úspěšný database.importIntoDatabaseSuccessed=Import do databáze byl úspěšný
database.backupCreated=Database backup successful
database.fileNotFound=File not Found database.fileNotFound=File not Found
database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný database.fileNullOrEmpty=Soubor nemůže být null nebo prázdný
database.failedImportFile=Failed Import File database.failedImportFile=Failed Import File

View File

@@ -238,11 +238,13 @@ database.creationDate=Oprettelsesdato
database.fileSize=Filstørrelse database.fileSize=Filstørrelse
database.deleteBackupFile=Slet Backup-fil database.deleteBackupFile=Slet Backup-fil
database.importBackupFile=Importér Backup-fil database.importBackupFile=Importér Backup-fil
database.createBackupFile=Create Backup File
database.downloadBackupFile=Download Backup-fil database.downloadBackupFile=Download Backup-fil
database.info_1=Ved import af data er det afgørende at sikre den korrekte struktur. Hvis du er usikker på, hvad du gør, søg råd og støtte fra en professionel. En fejl i strukturen kan forårsage applikationsfejl, op til og med fuldstændig manglende evne til at køre applikationen. database.info_1=Ved import af data er det afgørende at sikre den korrekte struktur. Hvis du er usikker på, hvad du gør, søg råd og støtte fra en professionel. En fejl i strukturen kan forårsage applikationsfejl, op til og med fuldstændig manglende evne til at køre applikationen.
database.info_2=Filnavnet er ligegyldigt ved upload. Det vil blive omdøbt bagefter for at følge formatet backup_user_yyyyMMddHHmm.sql, hvilket sikrer en konsistent navngivningskonvention. database.info_2=Filnavnet er ligegyldigt ved upload. Det vil blive omdøbt bagefter for at følge formatet backup_user_yyyyMMddHHmm.sql, hvilket sikrer en konsistent navngivningskonvention.
database.submit=Importér Backup database.submit=Importér Backup
database.importIntoDatabaseSuccessed=Import i database lykkedes database.importIntoDatabaseSuccessed=Import i database lykkedes
database.backupCreated=Database backup successful
database.fileNotFound=Fil ikke fundet database.fileNotFound=Fil ikke fundet
database.fileNullOrEmpty=Fil må ikke være null eller tom database.fileNullOrEmpty=Fil må ikke være null eller tom
database.failedImportFile=Kunne ikke importere fil database.failedImportFile=Kunne ikke importere fil

View File

@@ -238,11 +238,13 @@ database.creationDate=Erstellungsdatum
database.fileSize=Dateigröße database.fileSize=Dateigröße
database.deleteBackupFile=Sicherungsdatei löschen database.deleteBackupFile=Sicherungsdatei löschen
database.importBackupFile=Sicherungsdatei importieren database.importBackupFile=Sicherungsdatei importieren
database.createBackupFile=Sicherungsdatei erstellen
database.downloadBackupFile=Sicherungsdatei herunterladen database.downloadBackupFile=Sicherungsdatei herunterladen
database.info_1=Beim Importieren der Daten ist es von größter Bedeutung, die korrekte Struktur zu gewährleisten. Wenn Sie nicht sicher sind, was Sie tun, suchen Sie Rat und Unterstützung von einem Fachmann. Ein Fehler in der Struktur kann zu Fehlfunktionen der Anwendung führen, bis hin zur vollständigen Nicht-Lauffähigkeit der Anwendung. database.info_1=Beim Importieren der Daten ist es von größter Bedeutung, die korrekte Struktur zu gewährleisten. Wenn Sie nicht sicher sind, was Sie tun, suchen Sie Rat und Unterstützung von einem Fachmann. Ein Fehler in der Struktur kann zu Fehlfunktionen der Anwendung führen, bis hin zur vollständigen Nicht-Lauffähigkeit der Anwendung.
database.info_2=Der Dateiname spielt beim Hochladen keine Rolle. Dieser wird nachträglich in das Format backup_user_yyyyMMddHHmm.sql geändert, um eine einheitliche Benennung zu gewährleisten. database.info_2=Der Dateiname spielt beim Hochladen keine Rolle. Dieser wird nachträglich in das Format backup_user_yyyyMMddHHmm.sql geändert, um eine einheitliche Benennung zu gewährleisten.
database.submit=Sicherungsdatei importieren database.submit=Sicherungsdatei importieren
database.importIntoDatabaseSuccessed=Import in die Datenbank erfolgreich database.importIntoDatabaseSuccessed=Import in die Datenbank erfolgreich
database.backupCreated=Datenbanksicherung erfolgreich
database.fileNotFound=Datei nicht gefunden database.fileNotFound=Datei nicht gefunden
database.fileNullOrEmpty=Datei darf nicht null oder leer sein database.fileNullOrEmpty=Datei darf nicht null oder leer sein
database.failedImportFile=Dateiimport fehlgeschlagen database.failedImportFile=Dateiimport fehlgeschlagen

View File

@@ -238,11 +238,13 @@ database.creationDate=Ημερομηνία δημιουργίας
database.fileSize=Μέγεθος αρχείου database.fileSize=Μέγεθος αρχείου
database.deleteBackupFile=Διαγραφή Προγράμματος Ανασυγκρότησης database.deleteBackupFile=Διαγραφή Προγράμματος Ανασυγκρότησης
database.importBackupFile=Εισάγωντας Προγράμματος Ανασυγκρότησης database.importBackupFile=Εισάγωντας Προγράμματος Ανασυγκρότησης
database.createBackupFile=Create Backup File
database.downloadBackupFile=Κατέβασμα Προγράμματος Ανασυγκρότησης database.downloadBackupFile=Κατέβασμα Προγράμματος Ανασυγκρότησης
database.info_1=Όταν εισάγετε δεδομένα, είναι σημαντικό να εξασφαλίσετε τη σωστή μορφοποίηση. Αν δεν είστε σίγουροι για το ποια πράγματα κάνετε, αιτήστε υποβολή και υποστήριξη από ευελίκτω. Μια σφάλμα στη μορφοποίηση μπορεί να προκαλέσει καταστάσεις λάθους στην εφαρμογή, αν όχι ολόκληρη την αποχώρηση της εφαρμογής. database.info_1=Όταν εισάγετε δεδομένα, είναι σημαντικό να εξασφαλίσετε τη σωστή μορφοποίηση. Αν δεν είστε σίγουροι για το ποια πράγματα κάνετε, αιτήστε υποβολή και υποστήριξη από ευελίκτω. Μια σφάλμα στη μορφοποίηση μπορεί να προκαλέσει καταστάσεις λάθους στην εφαρμογή, αν όχι ολόκληρη την αποχώρηση της εφαρμογής.
database.info_2=Το ονόμα του αρχείου δεν έχει σημασία όταν ξεκινάτε μια επέμβαση. Αλλαγήται αργότερα για να ακολουθήσει το σχήμα backup_ωςώντας_YYYYMMDDHHmm.sql, επιτρέποντας μια συνεχές κατάληψη ονόματος. database.info_2=Το ονόμα του αρχείου δεν έχει σημασία όταν ξεκινάτε μια επέμβαση. Αλλαγήται αργότερα για να ακολουθήσει το σχήμα backup_ωςώντας_YYYYMMDDHHmm.sql, επιτρέποντας μια συνεχές κατάληψη ονόματος.
database.submit=Εισάγωντας Προγράμματος Ανασυγκρότησης database.submit=Εισάγωντας Προγράμματος Ανασυγκρότησης
database.importIntoDatabaseSuccessed=Τελείωση εισαγωγής στη βάση δεδομένων. database.importIntoDatabaseSuccessed=Τελείωση εισαγωγής στη βάση δεδομένων.
database.backupCreated=Database backup successful
database.fileNotFound=File not Found database.fileNotFound=File not Found
database.fileNullOrEmpty=Το αρχείο δεν μπορεί να είναι τυχόν ή κενό. database.fileNullOrEmpty=Το αρχείο δεν μπορεί να είναι τυχόν ή κενό.
database.failedImportFile=Failed Import File database.failedImportFile=Failed Import File

View File

@@ -238,11 +238,13 @@ database.creationDate=Creation Date
database.fileSize=File Size database.fileSize=File Size
database.deleteBackupFile=Delete Backup File database.deleteBackupFile=Delete Backup File
database.importBackupFile=Import Backup File database.importBackupFile=Import Backup File
database.createBackupFile=Create Backup File
database.downloadBackupFile=Download Backup File database.downloadBackupFile=Download Backup File
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application.
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention.
database.submit=Import Backup database.submit=Import Backup
database.importIntoDatabaseSuccessed=Import into database successed database.importIntoDatabaseSuccessed=Import into database successed
database.backupCreated=Database backup successful
database.fileNotFound=File not found database.fileNotFound=File not found
database.fileNullOrEmpty=File must not be null or empty database.fileNullOrEmpty=File must not be null or empty
database.failedImportFile=Failed to import file database.failedImportFile=Failed to import file

View File

@@ -238,11 +238,13 @@ database.creationDate=Creation Date
database.fileSize=File Size database.fileSize=File Size
database.deleteBackupFile=Delete Backup File database.deleteBackupFile=Delete Backup File
database.importBackupFile=Import Backup File database.importBackupFile=Import Backup File
database.createBackupFile=Create Backup File
database.downloadBackupFile=Download Backup File database.downloadBackupFile=Download Backup File
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application. database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application.
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention. database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention.
database.submit=Import Backup database.submit=Import Backup
database.importIntoDatabaseSuccessed=Import into database successed database.importIntoDatabaseSuccessed=Import into database successed
database.backupCreated=Database backup successful
database.fileNotFound=File not Found database.fileNotFound=File not Found
database.fileNullOrEmpty=File must not be null or empty database.fileNullOrEmpty=File must not be null or empty
database.failedImportFile=Failed Import File database.failedImportFile=Failed Import File

View File

@@ -238,11 +238,13 @@ database.creationDate=Fecha de creación
database.fileSize=Tamaño de archivo database.fileSize=Tamaño de archivo
database.deleteBackupFile=Eliminar archivo de copia de seguridad database.deleteBackupFile=Eliminar archivo de copia de seguridad
database.importBackupFile=Importar archivo de copia de seguridad database.importBackupFile=Importar archivo de copia de seguridad
database.createBackupFile=Create Backup File
database.downloadBackupFile=Descargar archivo de copia de seguridad database.downloadBackupFile=Descargar archivo de copia de seguridad
database.info_1=Al importar datos, es fundamental garantizar la estructura correcta. Si no está seguro de lo que está haciendo, busque consejo y apoyo de un profesional. Un error en la estructura puede causar un mal funcionamiento de la aplicación, incluyendo la imposibilidad total de ejecutar la aplicación. database.info_1=Al importar datos, es fundamental garantizar la estructura correcta. Si no está seguro de lo que está haciendo, busque consejo y apoyo de un profesional. Un error en la estructura puede causar un mal funcionamiento de la aplicación, incluyendo la imposibilidad total de ejecutar la aplicación.
database.info_2=El nombre del archivo no importa al cargarlo. Posteriormente se le cambiará el nombre para que siga el formato backup_user_yyyyMMddHHmm.sql, lo que garantiza una convención de nomenclatura coherente. database.info_2=El nombre del archivo no importa al cargarlo. Posteriormente se le cambiará el nombre para que siga el formato backup_user_yyyyMMddHHmm.sql, lo que garantiza una convención de nomenclatura coherente.
database.submit=Importar Copia de Seguridad database.submit=Importar Copia de Seguridad
database.importIntoDatabaseSuccessed=Importación a la base de datos ha sido exitosa database.importIntoDatabaseSuccessed=Importación a la base de datos ha sido exitosa
database.backupCreated=Database backup successful
database.fileNotFound=Archivo no encontrado database.fileNotFound=Archivo no encontrado
database.fileNullOrEmpty=El archivo no debe ser nulo o vacío. database.fileNullOrEmpty=El archivo no debe ser nulo o vacío.
database.failedImportFile=Archivo de importación fallido database.failedImportFile=Archivo de importación fallido

Some files were not shown because too many files have changed in this diff Show More