Compare commits

..

9 Commits

Author SHA1 Message Date
Anthony Stirling
768cb20e75 Update check_properties.yml 2024-12-21 01:02:25 +00:00
Anthony Stirling
339bbbf4b1 Update swagger.yml 2024-12-21 01:00:46 +00:00
Anthony Stirling
475036d12e Update build.yml 2024-12-21 01:00:37 +00:00
Anthony Stirling
b1fa2246a1 Create codeql.yml 2024-12-21 00:58:18 +00:00
Anthony Stirling
abaab580be Update PR-Demo-Comment.yml 2024-12-21 00:50:12 +00:00
Anthony Stirling
1d700ae062 Update PR-Demo-Comment.yml 2024-12-21 00:49:06 +00:00
Anthony Stirling
bba3d65368 Create SECURITY.md 2024-12-21 00:23:58 +00:00
Anthony Stirling
e3d2bd3b1b Merge pull request #2513 from PeterDaveHelloKitchen/zh_TW
Update and improve zh_TW Traditional Chinese locale
2024-12-20 17:05:36 +00:00
Peter Dave Hello
26340626df Update and improve zh_TW Traditional Chinese locale 2024-12-20 03:55:31 +08:00
100 changed files with 1756 additions and 1359 deletions

View File

@@ -3,7 +3,9 @@ name: PR Deployment via Comment
on:
issue_comment:
types: [created]
permissions:
pull-requests: write
contents: write
jobs:
check-comment:
runs-on: ubuntu-latest

View File

@@ -54,7 +54,8 @@ jobs:
# )
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Repository
uses: actions/checkout@v4

View File

@@ -14,6 +14,9 @@ jobs:
check-files:
if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout main branch first
uses: actions/checkout@v4

48
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '30 1 * * 0' # Runs at 01:30 UTC every Sunday
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
timeout-minutes: 360
permissions:
actions: read
contents: read
security-events: false
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: security-extended,security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -5,7 +5,8 @@ on:
push:
branches:
- master
permissions:
contents: read
jobs:
push:
runs-on: ubuntu-latest

63
SECURITY.md Normal file
View File

@@ -0,0 +1,63 @@
# Security Policy
## Reporting a Vulnerability
The Stirling-PDF team takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings.
### How to Report
You can report security vulnerabilities through two channels:
1. **GitHub Security Advisory**:
- Navigate to the [Security tab](https://github.com/Stirling-Tools/Stirling-PDF/security) in our repository
- Click on "Report a vulnerability"
- Provide a detailed description of the vulnerability
2. **Direct Email**:
- Send your report to security@stirlingpdf.com
- Please include as much information as possible about the vulnerability
### What to Include
When reporting a vulnerability, please provide:
- A clear description of the vulnerability
- Steps to reproduce the issue
- Any potential impact
- If possible, suggestions for addressing the vulnerability
- Your contact information for follow-up questions
### Response Time
We aim to acknowledge receipt of your vulnerability report within 48 hours
### Process
1. Submit your report through one of the channels above
2. Receive an acknowledgment from our team
3. Our team will investigate and validate the issue
4. We will work on a fix and keep you updated on our progress
5. Once resolved, we will publish the fix and acknowledge your contribution (if desired)
### Bug Bounty
At this time, we do not offer a bug bounty program. However, we greatly appreciate your efforts in making Stirling-PDF more secure and will acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
## Supported Versions
Only the latest version of Stirling-PDF is supported for security updates. We do not backport security fixes to older versions.
| Version | Supported |
| ------- | ------------------ |
| Latest | :white_check_mark: |
| Older | :x: |
**Please note:** Before reporting a security issue, ensure you are using the latest version of Stirling-PDF. Security reports for older versions will not be accepted.
## Security Best Practices
When deploying Stirling-PDF:
1. Always use the latest version
2. Follow our deployment guidelines
3. Regularly check for and apply updates

View File

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

View File

@@ -51,13 +51,15 @@ public class TSAClient {
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
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 String username;
private final String password;
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 username user name of TSA

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@@ -13,15 +14,8 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
public class EEAppConfig {
private final ApplicationProperties applicationProperties;
private final LicenseKeyChecker licenseKeyChecker;
public EEAppConfig(
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker;
}
@Autowired ApplicationProperties applicationProperties;
@Autowired private LicenseKeyChecker licenseKeyChecker;
@Bean(name = "runningEE")
public boolean runningEnterpriseEdition() {

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import java.nio.file.Paths;
import java.util.Properties;
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.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@@ -26,11 +27,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
public class AppConfig {
private final ApplicationProperties applicationProperties;
public AppConfig(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Autowired ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(
@@ -109,11 +106,13 @@ public class AppConfig {
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) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.config.security;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@@ -14,15 +15,8 @@ import stirling.software.SPDF.repository.UserRepository;
@Service
class AppUpdateAuthService implements ShowAdminInterface {
private final UserRepository userRepository;
private final ApplicationProperties applicationProperties;
public AppUpdateAuthService(
UserRepository userRepository, ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.applicationProperties = applicationProperties;
}
@Autowired private UserRepository userRepository;
@Autowired private ApplicationProperties applicationProperties;
@Override
public boolean getShowUpdateOnlyAdmins() {
@@ -30,18 +24,24 @@ class AppUpdateAuthService implements ShowAdminInterface {
if (!showUpdate) {
return showUpdate;
}
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return !showUpdateOnlyAdmin;
}
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
return !showUpdateOnlyAdmin;
}
Optional<User> user = userRepository.findByUsername(authentication.getName());
if (user.isPresent() && showUpdateOnlyAdmin) {
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
}
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="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.*;
import java.sql.Connection;
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.ZoneId;
import java.time.format.DateTimeFormatter;

View File

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

View File

@@ -1,7 +1,12 @@
package stirling.software.SPDF.config.security.session;
import java.time.Duration;
import java.util.*;
import java.util.ArrayList;
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.security.core.session.SessionInformation;

View File

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

View File

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

View File

@@ -8,13 +8,18 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import org.eclipse.jetty.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
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.servlet.mvc.support.RedirectAttributes;
@@ -33,11 +38,7 @@ import stirling.software.SPDF.config.security.database.DatabaseBackupHelper;
@Tag(name = "Database", description = "Database APIs for backup, import, and management")
public class DatabaseController {
private final DatabaseBackupHelper databaseBackupHelper;
public DatabaseController(DatabaseBackupHelper databaseBackupHelper) {
this.databaseBackupHelper = databaseBackupHelper;
}
@Autowired DatabaseBackupHelper databaseBackupHelper;
@Operation(
summary = "Import a database backup file",
@@ -49,11 +50,13 @@ public class DatabaseController {
MultipartFile file,
RedirectAttributes redirectAttributes)
throws IOException {
if (file == null || file.isEmpty()) {
redirectAttributes.addAttribute("error", "fileNullOrEmpty");
return "redirect:/database";
}
log.info("Received file: {}", file.getOriginalFilename());
Path tempTemplatePath = Files.createTempFile("backup_", ".sql");
try (InputStream in = file.getInputStream()) {
Files.copy(in, tempTemplatePath, StandardCopyOption.REPLACE_EXISTING);
@@ -79,9 +82,11 @@ public class DatabaseController {
@Parameter(description = "Name of the file to import", required = true) @PathVariable
String fileName)
throws IOException {
if (fileName == null || fileName.isEmpty()) {
return "redirect:/database?error=fileNullOrEmpty";
}
// Check if the file exists in the backup list
boolean fileExists =
databaseBackupHelper.getBackupList().stream()
@@ -91,6 +96,7 @@ public class DatabaseController {
return "redirect:/database?error=fileNotFound";
}
log.info("Received file: {}", fileName);
if (databaseBackupHelper.importDatabaseFromUI(fileName)) {
log.info("File {} imported to database", fileName);
return "redirect:/database?infoMessage=importIntoDatabaseSuccessed";
@@ -106,6 +112,7 @@ public class DatabaseController {
public String deleteFile(
@Parameter(description = "Name of the file to delete", required = true) @PathVariable
String fileName) {
if (fileName == null || fileName.isEmpty()) {
throw new IllegalArgumentException("File must not be null or empty");
}

View File

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

View File

@@ -6,10 +6,7 @@ import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
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.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

View File

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

View File

@@ -49,74 +49,6 @@ public class SplitPdfByChaptersController {
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")
@Operation(
summary = "Split PDFs by Chapters",
@@ -231,6 +163,74 @@ public class SplitPdfByChaptersController {
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(
List<Bookmark> bookmarks, List<ByteArrayOutputStream> splitDocumentsBoas)
throws Exception {

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.google.zxing.*;
import com.google.zxing.BinaryBitmap;
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 io.github.pixee.security.Filenames;
@@ -51,52 +56,6 @@ public class AutoSplitPdfController {
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")
@Operation(
summary = "Auto split PDF pages into separate documents",
@@ -195,4 +154,50 @@ 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,32 +47,6 @@ public class BlankPageController {
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")
@Operation(
summary = "Remove blank pages from a PDF file",
@@ -169,4 +143,30 @@ public class BlankPageController {
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;
import java.awt.*;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.file.Files;

View File

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

View File

@@ -1,10 +1,19 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.*;
import java.awt.AlphaComposite;
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.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.image.*;
import java.awt.image.AffineTransformOp;
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.IOException;
import java.security.SecureRandom;

View File

@@ -13,7 +13,11 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.InitBinder;
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 io.github.pixee.security.Filenames;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,20 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.*;
import java.io.*;
import java.awt.Color;
import java.io.ByteArrayInputStream;
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.security.*;
import java.security.KeyStore;
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.CertificateException;
import java.security.cert.CertificateFactory;
@@ -80,151 +91,6 @@ public class CertSignController {
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 {
File logoFile;
@@ -332,4 +198,149 @@ 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,13 +4,25 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Calendar;
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.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.PDDocument;
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.PDRectangle;
import org.apache.pdfbox.pdmodel.common.PDStream;
@@ -71,48 +83,6 @@ public class GetInfoOnPDF {
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")
@Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> getPdfInfo(@ModelAttribute PDFFile request) throws IOException {
@@ -636,6 +606,21 @@ public class GetInfoOnPDF {
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) {
if (width > height) {
return "Landscape";
@@ -693,6 +678,33 @@ public class GetInfoOnPDF {
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) {
ArrayNode elementsArray = objectMapper.createArrayNode();
if (nodes != null) {

View File

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

View File

@@ -4,9 +4,17 @@ import java.io.IOException;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.PDDocument;
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.interactive.action.*;
import org.apache.pdfbox.pdmodel.interactive.action.PDAction;
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.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;

View File

@@ -14,7 +14,11 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.*;
import org.bouncycastle.cms.CMSProcessable;
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.util.Store;
import org.springframework.beans.factory.annotation.Autowired;

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,12 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.ArrayList;
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.Stream;
@@ -34,41 +39,31 @@ import stirling.software.SPDF.service.SignatureService;
@Slf4j
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")
@Hidden
public String pipelineForm(Model model) {
model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>();
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
if (new File("./pipeline/defaultWebUIConfigs/").exists()) {
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
List<Path> jsonFiles =
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".json"))
.collect(Collectors.toList());
for (Path jsonFile : jsonFiles) {
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
pipelineConfigs.add(content);
}
for (String config : pipelineConfigs) {
Map<String, Object> jsonContent =
new ObjectMapper()
.readValue(config, new TypeReference<Map<String, Object>>() {});
String name = (String) jsonContent.get("name");
if (name == null || name.length() < 1) {
String filename =
@@ -83,6 +78,7 @@ public class GeneralWebController {
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
} catch (IOException e) {
log.error("exception", e);
}
@@ -94,7 +90,9 @@ public class GeneralWebController {
pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs);
return "pipeline";
}
@@ -175,6 +173,14 @@ public class GeneralWebController {
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")
@Hidden
public String signForm(Model model) {
@@ -182,8 +188,10 @@ public class GeneralWebController {
if (userService != null) {
username = userService.getCurrentUsername();
}
// Get signatures from both personal and ALL_USERS folders
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
model.addAttribute("currentPage", "sign");
model.addAttribute("fonts", getFontNames());
model.addAttribute("signatures", signatures);
@@ -218,12 +226,17 @@ public class GeneralWebController {
return "overlay-pdf";
}
@Autowired private ResourceLoader resourceLoader;
private List<FontResource> getFontNames() {
List<FontResource> fontNames = new ArrayList<>();
// Extract font names from classpath
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
// Extract font names from external directory
fontNames.addAll(getFontNamesFromLocation("file:customFiles/static/fonts/*"));
return fontNames;
}
@@ -270,38 +283,13 @@ public class GeneralWebController {
case "svg":
return "svg";
default:
// or throw an exception if an unexpected extension is encountered
return "";
return ""; // or throw an exception if an unexpected extension is encountered
}
}
@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 {
private String name;
private String extension;
private String type;
public FontResource(String name, String extension) {
@@ -334,4 +322,25 @@ public class GeneralWebController {
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,6 +6,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
@@ -27,12 +28,6 @@ import stirling.software.SPDF.model.Dependency;
@Slf4j
public class HomeWebController {
private final ApplicationProperties applicationProperties;
public HomeWebController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@GetMapping("/about")
@Hidden
public String gameForm(Model model) {
@@ -74,6 +69,8 @@ public class HomeWebController {
return "redirect:/";
}
@Autowired ApplicationProperties applicationProperties;
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
@Hidden

View File

@@ -5,6 +5,7 @@ import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
@@ -29,18 +30,12 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
public class MetricsController {
private final ApplicationProperties applicationProperties;
@Autowired ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry;
private boolean metricsEnabled;
public MetricsController(
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) {
this.applicationProperties = applicationProperties;
this.meterRegistry = meterRegistry;
}
@PostConstruct
public void init() {
Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled();
@@ -48,6 +43,11 @@ public class MetricsController {
this.metricsEnabled = metricsEnabled;
}
@Autowired
public MetricsController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@GetMapping("/status")
@Operation(
summary = "Application status and version",
@@ -57,6 +57,7 @@ public class MetricsController {
if (!metricsEnabled) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled.");
}
Map<String, String> status = new HashMap<>();
status.put("status", "UP");
status.put("version", getClass().getPackage().getImplementationVersion());
@@ -235,6 +236,7 @@ public class MetricsController {
String uri = counter.getId().getTag("uri");
counts.merge(uri, counter.count(), Double::sum);
});
List<EndpointCount> result =
counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
@@ -269,6 +271,7 @@ public class MetricsController {
private List<EndpointCount> getUniqueUserCounts(String method) {
log.info("Getting unique user counts for method: {}", method);
Map<String, Set<String>> uniqueUsers = new HashMap<>();
meterRegistry
.find("http.requests")
.tag("method", method)
@@ -281,37 +284,19 @@ public class MetricsController {
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
}
});
List<EndpointCount> result =
uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
log.info("Found {} endpoints with unique user counts", result.size());
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 {
private String endpoint;
private double count;
public EndpointCount(String endpoint, double count) {
@@ -335,4 +320,23 @@ public class MetricsController {
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,6 +6,7 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@@ -21,11 +22,7 @@ import stirling.software.SPDF.utils.CheckProgramInstall;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class OtherWebController {
private final ApplicationProperties applicationProperties;
public OtherWebController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Autowired ApplicationProperties applicationProperties;
@GetMapping("/compress-pdf")
@Hidden

View File

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

View File

@@ -79,23 +79,6 @@ public class ApplicationProperties {
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 {
ALL("all"),
NORMAL("normal"),
@@ -114,6 +97,23 @@ 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
public static class InitialLogin {
private String username;

View File

@@ -2,7 +2,14 @@ package stirling.software.SPDF.model;
import java.io.Serializable;
import jakarta.persistence.*;
import jakarta.persistence.Column;
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
@Table(name = "authorities")
@@ -10,6 +17,14 @@ public class Authority implements Serializable {
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
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@@ -21,14 +36,6 @@ public class Authority implements Serializable {
@JoinColumn(name = "user_id")
private User user;
public Authority() {}
public Authority(String authority, User user) {
this.authority = authority;
this.user = user;
user.getAuthorities().add(this);
}
public Long getId() {
return id;
}

View File

@@ -40,6 +40,22 @@ public enum Role {
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) {
// Using the fromString method to get the Role enum based on the roleId
Role role = fromString(roleId);
@@ -65,20 +81,4 @@ public enum Role {
}
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;
}
public String getAuthenticationType() {
return authenticationType;
}
public void setAuthenticationType(AuthenticationType authenticationType) {
this.authenticationType = authenticationType.toString().toLowerCase();
}
public String getAuthenticationType() {
return authenticationType;
}
public Set<Authority> getAuthorities() {
return authorities;
}

View File

@@ -12,10 +12,6 @@ public class GithubProvider extends Provider {
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 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() {
return authorizationUri;
@@ -29,6 +25,11 @@ public class GithubProvider extends Provider {
return userInfoUri;
}
private String clientId;
private String clientSecret;
private Collection<String> scopes = new ArrayList<>();
private String useAsUsername = "login";
@Override
public String getIssuer() {
return new String();

View File

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

View File

@@ -21,6 +21,16 @@ public class TextFinder extends PDFTextStripper {
private final boolean wholeWordSearch;
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)
throws IOException {
this.searchText = searchText.toLowerCase();
@@ -93,14 +103,4 @@ public class TextFinder extends PDFTextStripper {
return textOccurrences;
}
private class MatchInfo {
int startIndex;
int matchLength;
MatchInfo(int startIndex, int matchLength) {
this.startIndex = startIndex;
this.matchLength = matchLength;
}
}
}

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.repository;
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.PersistentTokenRepository;
import org.springframework.transaction.annotation.Transactional;
@@ -10,11 +11,7 @@ import stirling.software.SPDF.model.PersistentLogin;
public class JPATokenRepositoryImpl implements PersistentTokenRepository {
private final PersistentLoginRepository persistentLoginRepository;
public JPATokenRepositoryImpl(PersistentLoginRepository persistentLoginRepository) {
this.persistentLoginRepository = persistentLoginRepository;
}
@Autowired private PersistentLoginRepository persistentLoginRepository;
@Override
@Transactional

View File

@@ -1,10 +1,25 @@
package stirling.software.SPDF.service;
import java.io.*;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.*;
import java.util.*;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
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;

View File

@@ -6,7 +6,11 @@ import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -11,14 +11,15 @@ import lombok.Data;
@AllArgsConstructor
@Data
public class FileInfo {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private String fileName;
private String filePath;
private LocalDateTime modificationDate;
private long fileSize;
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.
public Path getFilePathAsPath() {
return Paths.get(filePath);

View File

@@ -1,6 +1,11 @@
package stirling.software.SPDF.utils;
import java.io.*;
import java.io.ByteArrayInputStream;
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.file.FileVisitResult;
import java.nio.file.Files;

View File

@@ -4,9 +4,18 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.net.HttpURLConnection;
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.file.*;
import java.nio.file.FileVisitResult;
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.security.MessageDigest;
import java.util.ArrayList;

View File

@@ -1,7 +1,11 @@
package stirling.software.SPDF.utils;
import java.awt.geom.AffineTransform;
import java.awt.image.*;
import java.awt.image.AffineTransformOp;
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.InputStream;
import java.nio.ByteBuffer;

View File

@@ -126,6 +126,82 @@ public class PdfUtils {
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(
byte[] inputStream,
String imageType,
@@ -442,82 +518,6 @@ public class PdfUtils {
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. */
private record PdfRenderSettingsKey(float mediaBoxWidth, float mediaBoxHeight, int rotation) {}

View File

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

View File

@@ -14,10 +14,7 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree;
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.pdmodel.font.*;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;

View File

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

View File

@@ -11,8 +11,8 @@
<appender name="AUTHLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/invalid-auths.log</file>
<encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover and keep 7 days' worth of history -->
@@ -21,12 +21,12 @@
</rollingPolicy>
</appender>
<!-- Rolling File Appender -->
<!-- Rolling File Appender -->
<appender name="GENERAL" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/info.log</file>
<encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder>
<pattern>%d %p %c{1} [%thread] %m%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- daily rollover and keep 7 days' worth of history -->
@@ -43,8 +43,7 @@
</root>
<!-- 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="AUTHLOG"/>
</logger>

View File

@@ -81,7 +81,7 @@ page=頁面
pages=頁面
loading=載入中...
addToDoc=新增至文件
reset=Reset
reset=重設
legal.privacy=隱私權政策
legal.terms=使用條款
@@ -142,7 +142,7 @@ navbar.language=語言
navbar.settings=設定
navbar.allTools=工具
navbar.multiTool=複合工具
navbar.search=Search
navbar.search=搜尋
navbar.sections.organize=整理
navbar.sections.convertTo=轉換為 PDF
navbar.sections.convertFrom=從 PDF 轉換
@@ -238,13 +238,13 @@ database.creationDate=建立日期
database.fileSize=檔案大小
database.deleteBackupFile=刪除備份檔案
database.importBackupFile=匯入備份檔案
database.createBackupFile=Create Backup File
database.createBackupFile=建立備份檔案
database.downloadBackupFile=下載備份檔案
database.info_1=在匯入資料時,確保正確的結構至關重要。如果您不確定自己在做什麼,請尋求專業人士的建議和支援。結構錯誤可能會導致應用程式故障,甚至完全無法執行應用程式。
database.info_2=上傳時檔案名稱並不重要。上傳後將重新命名為 backup_user_yyyyMMddHHmm.sql 格式,以確保命名規範一致。
database.submit=匯入備份
database.importIntoDatabaseSuccessed=成功匯入資料庫
database.backupCreated=Database backup successful
database.backupCreated=資料庫備份成功
database.fileNotFound=找不到檔案
database.fileNullOrEmpty=檔案不得為空或空白
database.failedImportFile=匯入檔案失敗
@@ -255,7 +255,7 @@ session.refreshPage=重新整理頁面
#############
# HOME-PAGE #
#############
home.desc=的本機主機一站式 PDF 需求解決方案。
home.desc=的本機一站式 PDF 解決方案。
home.searchBar=搜尋功能...
@@ -514,9 +514,9 @@ home.splitPdfByChapters.title=依章節分割 PDF
home.splitPdfByChapters.desc=根據 PDF 的章節結構將其分割成多個檔案。
splitPdfByChapters.tags=分割,章節,書籤,整理
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
home.validateSignature.title=驗證 PDF 簽章
home.validateSignature.desc=驗證 PDF 文件中的數位簽章與憑證
validateSignature.tags=簽章,驗證,確認,pdf,憑證,數位簽章,驗證簽章,驗證憑證
#replace-invert-color
replace-color.title=取代-反轉顏色
@@ -629,12 +629,12 @@ HTMLToPDF.help=接受 HTML 文件和包含所需 html/css/images 等的 ZIP
HTMLToPDF.submit=轉換
HTMLToPDF.credit=此服務使用 WeasyPrint 進行轉換
HTMLToPDF.zoom=用於顯示網站的縮放級別。
HTMLToPDF.pageWidth=頁面寬度-以釐米為單位(空則使用預設值)
HTMLToPDF.pageHeight=頁面高度-以釐米為單位(空則使用預設值)
HTMLToPDF.marginTop=頁面的上邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginBottom=頁面的下邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginLeft=頁面的左邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginRight=頁面的右邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.pageWidth=頁面寬度-以公分為單位(空則使用預設值)
HTMLToPDF.pageHeight=頁面高度-以公分為單位(空則使用預設值)
HTMLToPDF.marginTop=頁面的上邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginBottom=頁面的下邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginLeft=頁面的左邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.marginRight=頁面的右邊距-以毫米為單位(空則使用預設值)
HTMLToPDF.printBackground=渲染網站的背景。
HTMLToPDF.defaultHeader=啟用預設標頭(名稱和頁碼)
HTMLToPDF.cssMediaType=更改頁面的 CSS 媒體類型。
@@ -748,13 +748,13 @@ scalePages.submit=送出
certSign.title=憑證簽章
certSign.header=使用你的憑證簽章(進行中)
certSign.selectPDF=選擇要簽章的 PDF 檔案:
certSign.jksNote=注意:如果你的證書類型未在下面列出,請使用 keytool 命令列工具將其轉換為 Java Keystore .jks然後選擇下面的 .jks 文件選項。
certSign.selectKey=選擇你的私鑰文件PKCS#8 格式,可能是 .pem 或 .der
certSign.selectCert=選擇你的憑證文件X.509 格式,可能是 .pem 或 .der
certSign.selectP12=選擇你的 PKCS#12 金鑰庫文件(.p12 或 .pfx選,如果提供,它應包含你的私鑰和憑證):
certSign.selectJKS=選擇你的 Java Keystore 檔 .jks 或 .keystore
certSign.jksNote=注意:如果你的證書類型未被列在下方,請使用 keytool 命令列工具將其轉換為 Java Keystore .jks案格式,然後選擇下面的 .jks 檔案選項。
certSign.selectKey=選擇你的私鑰檔案PKCS#8 格式,副檔名可能是 .pem 或 .der
certSign.selectCert=選擇你的憑證檔案X.509 格式,副檔名可能是 .pem 或 .der
certSign.selectP12=選擇你的 PKCS#12 金鑰庫檔案(副檔名可能是 .p12 或 .pfx,如果提供,它應包含你的私鑰和憑證):
certSign.selectJKS=選擇你的 Java Keystore 檔 副檔名可能是 .jks 或 .keystore
certSign.certType=憑證類型
certSign.password=輸入你的金鑰庫或私鑰密碼(如果有):
certSign.password=輸入你的金鑰庫或私鑰密碼(如果有的話
certSign.showSig=顯示簽章
certSign.reason=原因
certSign.location=位置
@@ -824,12 +824,12 @@ sign.save=儲存簽章
sign.personalSigs=個人簽章
sign.sharedSigs=共用簽章
sign.noSavedSigs=尚未儲存任何簽章
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
sign.addToAll=新增至所有頁面
sign.delete=刪除
sign.first=第一頁
sign.last=最後一頁
sign.next=下一頁
sign.previous=上一頁
#repair
repair.title=修復
@@ -946,39 +946,39 @@ pdfOrganiser.placeholder=(例如 1,3,2 或 4-8,2,10-12 或 2n-1
multiTool.title=PDF 複合工具
multiTool.header=PDF 複合工具
multiTool.uploadPrompts=檔名
multiTool.selectAll=Select All
multiTool.deselectAll=Deselect All
multiTool.selectPages=Page Select
multiTool.selectedPages=Selected Pages
multiTool.page=Page
multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected
multiTool.selectAll=全選
multiTool.deselectAll=取消全選
multiTool.selectPages=選取頁面
multiTool.selectedPages=已選取的頁面
multiTool.page=頁面
multiTool.deleteSelected=刪除已選取的項目
multiTool.downloadAll=匯出
multiTool.downloadSelected=匯出已選取的項目
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
multiTool.insertPageBreak=插入分頁符號
multiTool.addFile=新增檔案
multiTool.rotateLeft=向左旋轉
multiTool.rotateRight=向右旋轉
multiTool.split=分割
multiTool.moveLeft=向左移動
multiTool.moveRight=向右移動
multiTool.delete=刪除
multiTool.dragDropMessage=已選取的頁面
multiTool.undo=復原
multiTool.redo=重做
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
decrypt.passwordPrompt=此檔案已受密碼保護。請輸入密碼:
decrypt.cancelled=已取消處理 PDF{0}
decrypt.noPassword=未提供加密 PDF 的密碼:{0}
decrypt.invalidPassword=請重新輸入正確的密碼。
decrypt.invalidPasswordHeader=密碼錯誤或不支援的加密方式,PDF{0}
decrypt.unexpectedError=處理檔案時發生錯誤。請再試一次。
decrypt.serverError=解密時發生伺服器錯誤:{0}
decrypt.success=檔案已成功解密。
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
multiTool-advert.message=此功能也可以在我們的<a href="{0}">複合工具頁面</a>中使用。前往查看並體驗更強大的逐頁操作介面及其他進階功能!
#view pdf
viewPdf.title=檢視 PDF
@@ -1195,7 +1195,7 @@ split-by-size-or-count.submit=送出
#overlay-pdfs
overlay-pdfs.header=覆蓋 PDF 檔案
overlay-pdfs.baseFile.label=選擇基 PDF 檔案
overlay-pdfs.baseFile.label=選擇基 PDF 檔案
overlay-pdfs.overlayFiles.label=選擇覆蓋 PDF 檔案
overlay-pdfs.mode.label=選擇覆蓋模式
overlay-pdfs.mode.sequential=序列覆蓋
@@ -1281,49 +1281,49 @@ splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多
splitByChapters.submit=分割 PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
fileChooser.click=點選
fileChooser.or=
fileChooser.dragAndDrop=拖放檔案
fileChooser.hoveredDragAndDrop=將檔案拖放至此
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
releases.footer=版本資訊
releases.title=版本資訊
releases.header=版本資訊
releases.current.version=目前版本
releases.note=版本資訊僅提供英文版本
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits
validateSignature.title=驗證 PDF 簽章
validateSignature.header=驗證數位簽章
validateSignature.selectPDF=選擇已簽章的 PDF 檔案
validateSignature.submit=驗證簽章
validateSignature.results=驗證結果
validateSignature.status=狀態
validateSignature.signer=簽署者
validateSignature.date=日期
validateSignature.reason=原因
validateSignature.location=位置
validateSignature.noSignatures=此文件中未找到數位簽章
validateSignature.status.valid=有效
validateSignature.status.invalid=無效
validateSignature.chain.invalid=憑證鏈驗證失敗 - 無法驗證簽署者身份
validateSignature.trust.invalid=憑證不在信任儲存區中 - 無法驗證來源
validateSignature.cert.expired=憑證已過期
validateSignature.cert.revoked=憑證已被撤銷
validateSignature.signature.info=簽章資訊
validateSignature.signature=簽章
validateSignature.signature.mathValid=簽章在數學上有效,但:
validateSignature.selectCustomCert=自訂 X.509 憑證檔案(選填)
validateSignature.cert.info=憑證詳細資訊
validateSignature.cert.issuer=發行者
validateSignature.cert.subject=主旨
validateSignature.cert.serialNumber=序號
validateSignature.cert.validFrom=有效期自
validateSignature.cert.validUntil=有效期至
validateSignature.cert.algorithm=演算法
validateSignature.cert.keySize=金鑰長度
validateSignature.cert.version=版本
validateSignature.cert.keyUsage=金鑰用途
validateSignature.cert.selfSigned=自我簽署
validateSignature.cert.bits=位元

View File

@@ -15,7 +15,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.env.Environment;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.model.ApplicationProperties;
@ExtendWith(MockitoExtension.class)
@@ -26,12 +25,13 @@ public class SPdfApplicationTest {
@Mock
private ApplicationProperties applicationProperties;
@InjectMocks
private SPdfApplication sPdfApplication;
@BeforeEach
public void setUp() {
sPdfApplication = new SPdfApplication();
sPdfApplication.setServerPortStatic("8080");
}

View File

@@ -8,7 +8,6 @@ import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
@@ -17,10 +16,7 @@ import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertNotNull;import org.junit.jupiter.api.BeforeEach;
class RearrangePagesPDFControllerTest {
@Mock
@@ -57,7 +53,7 @@ class RearrangePagesPDFControllerTest {
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
assertNotNull(newPageOrder, "Returning null instead of page order list");
assertEquals(Arrays.asList(0, 3, 1, 4, 2), newPageOrder, "Page order doesn't match");
assertEquals(Arrays.asList(0,3,1,4,2), newPageOrder, "Page order doesn't match");
}
/**
@@ -70,14 +66,13 @@ class RearrangePagesPDFControllerTest {
List<Integer> newPageOrder = sut.oddEvenMerge(totalNumberOfPages);
assertNotNull(newPageOrder, "Returning null instead of page order list");
assertEquals(Arrays.asList(0, 3, 1, 4, 2, 5), newPageOrder, "Page order doesn't match");
assertEquals(Arrays.asList(0,3,1,4,2,5), newPageOrder, "Page order doesn't match");
}
/**
* Tests the behavior of the oddEvenMerge method with multiple test cases of multiple pages.
*
* @param totalNumberOfPages The total number of pages in the document.
* @param expectedPageOrder The expected order of the pages after rearranging.
* @param expectedPageOrder The expected order of the pages after rearranging.
*/
@ParameterizedTest
@CsvSource({

View File

@@ -4,11 +4,13 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.http.ResponseEntity;
import stirling.software.SPDF.controller.api.RearrangePagesPDFController;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
public class ConvertWebsiteToPdfTest {

View File

@@ -1,11 +1,12 @@
package stirling.software.SPDF.utils;
import org.junit.jupiter.api.Test;
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
public class FileToPdfTest {

View File

@@ -1,108 +1,105 @@
package stirling.software.SPDF.utils;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class GeneralUtilsTest {
@Test
void testParsePageListWithAll() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"all"}, 5, false);
assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithAll() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"all"}, 5, false);
assertEquals(List.of(0, 1, 2, 3, 4), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithAllOneBased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"all"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages.");
}
@Test
void nFunc() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"n"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages.");
}
@Test
void nFuncAdvanced() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, true);
//skip 0 as not valid
assertEquals(List.of(4,8), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithAllOneBased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"all"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvancedZero() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false);
//skip 0 as not valid
assertEquals(List.of(3,7), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced2() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, true);
// skip -1 as not valid
assertEquals(List.of(3,7), result, "4n-1 should do (0-1), (4-1), (8-1)");
}
@Test
void nFuncAdvanced3() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n+1"}, 9, true);
assertEquals(List.of(1,5,9), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced4() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"3+2n"}, 9, true);
assertEquals(List.of(3,5,7,9), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvancedZerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false);
assertEquals(List.of(3,7), result, "'All' keyword should return all pages.");
}
@Test
void nFunc() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"n"}, 5, true);
assertEquals(List.of(1, 2, 3, 4, 5), result, "'n' keyword should return all pages.");
}
@Test
void nFuncAdvanced2Zerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, false);
assertEquals(List.of(2,6), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithRangeOneBasedOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, true);
assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly.");
}
@Test
void nFuncAdvanced() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, true);
//skip 0 as not valid
assertEquals(List.of(4, 8), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithRangeZeroBaseOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, false);
assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, true);
assertEquals(List.of(1, 3, 7,8), result, "Range should be parsed correctly.");
}
@Test
void nFuncAdvancedZero() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false);
//skip 0 as not valid
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced2() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, true);
// skip -1 as not valid
assertEquals(List.of(3, 7), result, "4n-1 should do (0-1), (4-1), (8-1)");
}
@Test
void nFuncAdvanced3() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n+1"}, 9, true);
assertEquals(List.of(1, 5, 9), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced4() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"3+2n"}, 9, true);
assertEquals(List.of(3, 5, 7, 9), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvancedZerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n"}, 9, false);
assertEquals(List.of(3, 7), result, "'All' keyword should return all pages.");
}
@Test
void nFuncAdvanced2Zerobased() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"4n-1"}, 9, false);
assertEquals(List.of(2, 6), result, "'All' keyword should return all pages.");
}
@Test
void testParsePageListWithRangeOneBasedOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, true);
assertEquals(List.of(1, 2, 3), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeZeroBaseOutput() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1-3"}, 5, false);
assertEquals(List.of(0, 1, 2), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, true);
assertEquals(List.of(1, 3, 7, 8), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFullOutOfRange() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 5, true);
assertEquals(List.of(1, 3), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeZeroBaseOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, false);
assertEquals(List.of(0, 2, 6, 7), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeOneBasedOutputFullOutOfRange() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 5, true);
assertEquals(List.of(1, 3), result, "Range should be parsed correctly.");
}
@Test
void testParsePageListWithRangeZeroBaseOutputFull() {
List<Integer> result = GeneralUtils.parsePageList(new String[]{"1,3,7-8"}, 8, false);
assertEquals(List.of(0, 2, 6,7), result, "Range should be parsed correctly.");
}
}

View File

@@ -2,8 +2,8 @@ package stirling.software.SPDF.utils;
import org.junit.jupiter.api.Test;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.Color;
import static org.junit.jupiter.api.Assertions.*;
@@ -13,14 +13,14 @@ public class ImageProcessingUtilsTest {
void testConvertColorTypeToGreyscale() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
assertNotNull(convertedImage);
assertEquals(BufferedImage.TYPE_BYTE_GRAY, convertedImage.getType());
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
// Check if a pixel is correctly converted to greyscale
Color grey = new Color(convertedImage.getRGB(0, 0));
assertEquals(grey.getRed(), grey.getGreen());
@@ -31,14 +31,14 @@ public class ImageProcessingUtilsTest {
void testConvertColorTypeToBlackWhite() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
assertNotNull(convertedImage);
assertEquals(BufferedImage.TYPE_BYTE_BINARY, convertedImage.getType());
assertEquals(sourceImage.getWidth(), convertedImage.getWidth());
assertEquals(sourceImage.getHeight(), convertedImage.getHeight());
// Check if a pixel is converted correctly (binary image will be either black or white)
int rgb = convertedImage.getRGB(0, 0);
assertTrue(rgb == Color.BLACK.getRGB() || rgb == Color.WHITE.getRGB());
@@ -48,7 +48,7 @@ public class ImageProcessingUtilsTest {
void testConvertColorTypeToFullColor() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
assertNotNull(convertedImage);
@@ -59,7 +59,7 @@ public class ImageProcessingUtilsTest {
void testConvertColorTypeInvalid() {
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
fillImageWithColor(sourceImage, Color.RED);
BufferedImage convertedImage = ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
assertNotNull(convertedImage);

View File

@@ -1,12 +1,11 @@
package stirling.software.SPDF.utils;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import stirling.software.SPDF.model.PdfMetadata;
import java.io.IOException;
import java.util.Collections;
@@ -14,6 +13,9 @@ import java.util.HashSet;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.cos.COSName;
public class PdfUtilsTest {

View File

@@ -1,13 +1,13 @@
package stirling.software.SPDF.utils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class ProcessExecutorTest {

View File

@@ -1,10 +1,10 @@
package stirling.software.SPDF.utils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test;
public class RequestUriUtilsTest {
@Test

View File

@@ -1,5 +1,10 @@
package stirling.software.SPDF.utils;
import static org.junit.jupiter.api.Assertions.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
@@ -8,11 +13,6 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class WebResponseUtilsTest {
@Test
@@ -55,7 +55,7 @@ public class WebResponseUtilsTest {
assertNotNull(headers);
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
}
@@ -78,7 +78,7 @@ public class WebResponseUtilsTest {
assertNotNull(headers);
assertEquals(MediaType.TEXT_PLAIN, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());
@@ -102,7 +102,7 @@ public class WebResponseUtilsTest {
assertNotNull(headers);
assertEquals(MediaType.APPLICATION_PDF, headers.getContentType());
assertNotNull(headers.getContentDisposition());
} catch (IOException e) {
fail("Exception thrown: " + e.getMessage());