Merge branch 'main' into session_2025_03_22

This commit is contained in:
Ludy 2025-05-08 18:53:31 +02:00 committed by GitHub
commit 5c0b3e1812
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 513 additions and 61 deletions

View File

@ -27,18 +27,34 @@ Back End:
Security: Security:
- changed-files: - changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/DatabaseController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/EmailController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/H2SQLController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/DatabaseWebController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/UserController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/Email.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/exception/NoProviderFoundExceptionjava'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/BackupNotFoundException.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AttemptCounter.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/Authority.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/PersistentLogin.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/SessionEntity.java'
- any-glob-to-any-file: 'scripts/download-security-jar.sh' - any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: '.github/workflows/dependency-review.yml' - any-glob-to-any-file: '.github/workflows/dependency-review.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml' - any-glob-to-any-file: '.github/workflows/scorecards.yml'
API: API:
- changed-files: - changed-files:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/OpenApiConfig.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/api/**/*'
- any-glob-to-any-file: 'scripts/png_to_webp.py' - any-glob-to-any-file: 'scripts/png_to_webp.py'
- any-glob-to-any-file: 'split_photos.py' - any-glob-to-any-file: 'split_photos.py'
- any-glob-to-any-file: '.github/workflows/swagger.yml' - any-glob-to-any-file: '.github/workflows/swagger.yml'

View File

@ -48,7 +48,7 @@ jobs:
# Generate GitHub App token # Generate GitHub App token
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -135,7 +135,7 @@ jobs:
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

View File

@ -24,7 +24,7 @@ jobs:
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

View File

@ -22,7 +22,7 @@ jobs:
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

View File

@ -74,6 +74,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16 uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
with: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@ -30,7 +30,7 @@ jobs:
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ secrets.GH_APP_ID }} app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -63,7 +63,7 @@ jobs:
- name: Generate GitHub App Token - name: Generate GitHub App Token
id: generate-token id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
with: with:
app-id: ${{ vars.GH_APP_ID }} app-id: ${{ vars.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}

View File

@ -19,7 +19,7 @@ import java.time.Year
ext { ext {
springBootVersion = "3.4.5" springBootVersion = "3.4.5"
pdfboxVersion = "3.0.4" pdfboxVersion = "3.0.5"
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.38" lombokVersion = "1.18.38"
bouncycastleVersion = "1.80" bouncycastleVersion = "1.80"
@ -51,16 +51,20 @@ sourceSets {
main { main {
java { java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/interfaces/DatabaseInterface.java"
exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/DatabaseController.java" exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude "stirling/software/SPDF/controller/api/UserController.java" exclude "stirling/software/SPDF/controller/api/EmailController.java"
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java" exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java"
exclude "stirling/software/SPDF/controller/api/UserController.java"
exclude "stirling/software/SPDF/controller/web/AccountWebController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
exclude "stirling/software/SPDF/model/api/Email.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
exclude "stirling/software/SPDF/model/AttemptCounter.java" exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude "stirling/software/SPDF/model/Authority.java" exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/BackupNotFoundException.java" exclude "stirling/software/SPDF/model/exception/BackupNotFoundException.java"
exclude "stirling/software/SPDF/model/exception/NoProviderFoundException.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java" exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java" exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
@ -80,16 +84,8 @@ sourceSets {
java { java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/UserControllerTest.java"
exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java"
exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java"
exclude "stirling/software/SPDF/model/AttemptCounterTest.java" exclude "stirling/software/SPDF/controller/api/EmailControllerTest.java"
exclude "stirling/software/SPDF/model/AuthorityTest.java"
exclude "stirling/software/SPDF/model/PersistentLoginTest.java"
exclude "stirling/software/SPDF/model/SessionEntityTest.java"
exclude "stirling/software/SPDF/model/UserTest.java"
exclude "stirling/software/SPDF/repository/**" exclude "stirling/software/SPDF/repository/**"
} }
@ -461,6 +457,7 @@ dependencies {
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion"
implementation "org.springframework.session:spring-session-core:3.4.3" implementation "org.springframework.session:spring-session-core:3.4.3"
implementation "org.springframework:spring-jdbc:6.2.6" implementation "org.springframework:spring-jdbc:6.2.6"
@ -508,11 +505,11 @@ dependencies {
implementation "com.drewnoakes:metadata-extractor:2.19.0" implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.19.0" implementation "commons-io:commons-io:2.19.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8"
//general PDF //general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ("com.opencsv:opencsv:5.10") implementation ("com.opencsv:opencsv:5.11")
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion")
implementation "org.apache.pdfbox:preflight:$pdfboxVersion" implementation "org.apache.pdfbox:preflight:$pdfboxVersion"

View File

@ -0,0 +1,63 @@
package stirling.software.SPDF.config.security.mail;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.Email;
/**
* Service class responsible for sending emails, including those with attachments. It uses
* JavaMailSender to send the email and is designed to handle both the message content and file
* attachments.
*/
@Service
@RequiredArgsConstructor
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false)
public class EmailService {
private final JavaMailSender mailSender;
private final ApplicationProperties applicationProperties;
/**
* Sends an email with an attachment asynchronously. This method is annotated with @Async, which
* means it will be executed asynchronously.
*
* @param email The Email object containing the recipient, subject, body, and file attachment.
* @throws MessagingException If there is an issue with creating or sending the email.
*/
@Async
public void sendEmailWithAttachment(Email email) throws MessagingException {
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
MultipartFile file = email.getFileInput();
// Creates a MimeMessage to represent the email
MimeMessage message = mailSender.createMimeMessage();
// Helper class to set up the message content and attachments
MimeMessageHelper helper = new MimeMessageHelper(message, true);
// Sets the recipient, subject, body, and sender email
helper.addTo(email.getTo());
helper.setSubject(email.getSubject());
helper.setText(
email.getBody(),
true); // The "true" here indicates that the body contains HTML content.
helper.setFrom(mailProperties.getFrom());
// Adds the attachment to the email
helper.addAttachment(file.getOriginalFilename(), file);
// Sends the email via the configured mail sender
mailSender.send(message);
}
}

View File

@ -0,0 +1,54 @@
package stirling.software.SPDF.config.security.mail;
import java.util.Properties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
/**
* This configuration class provides the JavaMailSender bean, which is used to send emails. It reads
* email server settings from the configuration (ApplicationProperties) and configures the mail
* client (JavaMailSender).
*/
@Configuration
@Slf4j
@AllArgsConstructor
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false)
public class MailConfig {
private final ApplicationProperties applicationProperties;
@Bean
public JavaMailSender javaMailSender() {
ApplicationProperties.Mail mailProperties = applicationProperties.getMail();
// Creates a new instance of JavaMailSenderImpl, which is a Spring implementation
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(mailProperties.getHost());
mailSender.setPort(mailProperties.getPort());
mailSender.setUsername(mailProperties.getUsername());
mailSender.setPassword(mailProperties.getPassword());
mailSender.setDefaultEncoding("UTF-8");
// Retrieves the JavaMail properties to configure additional SMTP parameters
Properties props = mailSender.getJavaMailProperties();
// Enables SMTP authentication
props.put("mail.smtp.auth", "true");
// Enables STARTTLS to encrypt the connection if supported by the SMTP server
props.put("mail.smtp.starttls.enable", "true");
// Returns the configured mail sender, ready to send emails
return mailSender;
}
}

View File

@ -59,7 +59,8 @@ public class AnalysisController {
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO") description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
public Map<String, String> getDocumentProperties(@ModelAttribute PDFFile file) public Map<String, String> getDocumentProperties(@ModelAttribute PDFFile file)
throws IOException { throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) { // Load the document in read-only mode to prevent modifications and ensure the integrity of the original file.
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput(), true)) {
PDDocumentInformation info = document.getDocumentInformation(); PDDocumentInformation info = document.getDocumentInformation();
Map<String, String> properties = new HashMap<>(); Map<String, String> properties = new HashMap<>();
properties.put("title", info.getTitle()); properties.put("title", info.getTitle());

View File

@ -0,0 +1,57 @@
package stirling.software.SPDF.controller.api;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpStatus;
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 io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.mail.MessagingException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.mail.EmailService;
import stirling.software.SPDF.model.api.Email;
/**
* Controller for handling email-related API requests. This controller exposes an endpoint for
* sending emails with attachments.
*/
@RestController
@RequestMapping("/api/v1/general")
@RequiredArgsConstructor
@Slf4j
@Tag(name = "General", description = "General APIs")
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false)
public class EmailController {
private final EmailService emailService;
/**
* Endpoint to send an email with an attachment. This method consumes a multipart/form-data
* request containing the email details and attachment.
*
* @param email The Email object containing recipient address, subject, body, and file
* attachment.
* @return ResponseEntity with success or error message.
*/
@PostMapping(consumes = "multipart/form-data", value = "/send-email")
public ResponseEntity<String> sendEmailWithAttachment(@Valid @ModelAttribute Email email) {
try {
// Calls the service to send the email with attachment
emailService.sendEmailWithAttachment(email);
return ResponseEntity.ok("Email sent successfully");
} catch (MessagingException e) {
// Catches any messaging exception (e.g., invalid email address, SMTP server issues)
String errorMsg = "Failed to send email: " + e.getMessage();
log.error(errorMsg, e); // Logging the detailed error
// Returns an error response with status 500 (Internal Server Error)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorMsg);
}
}
}

View File

@ -25,7 +25,7 @@ import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ReplaceAndInvertColorController { public class ReplaceAndInvertColorController {
private ReplaceAndInvertColorService replaceAndInvertColorService; private final ReplaceAndInvertColorService replaceAndInvertColorService;
@PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf") @PostMapping(consumes = "multipart/form-data", value = "/replace-invert-pdf")
@Operation( @Operation(

View File

@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.security; package stirling.software.SPDF.controller.api.security;
import java.awt.*; import java.awt.*;
import java.beans.PropertyEditorSupport;
import java.io.*; import java.io.*;
import java.nio.file.Files; import java.nio.file.Files;
import java.security.*; import java.security.*;
@ -53,7 +54,10 @@ import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException; import org.bouncycastle.pkcs.PKCSException;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -82,6 +86,18 @@ public class CertSignController {
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(
MultipartFile.class,
new PropertyEditorSupport() {
@Override
public void setAsText(String text) throws IllegalArgumentException {
setValue(null);
}
});
}
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private static void sign( private static void sign(
@ -103,8 +119,7 @@ public class CertSignController {
signature.setLocation(location); signature.setLocation(location);
signature.setReason(reason); signature.setReason(reason);
signature.setSignDate(Calendar.getInstance()); signature.setSignDate(Calendar.getInstance());
if (Boolean.TRUE.equals(showSignature)) {
if (showSignature) {
SignatureOptions signatureOptions = new SignatureOptions(); SignatureOptions signatureOptions = new SignatureOptions();
signatureOptions.setVisualSignature( signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, showLogo)); instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
@ -121,7 +136,12 @@ public class CertSignController {
} }
} }
@PostMapping(consumes = "multipart/form-data", value = "/cert-sign") @PostMapping(
consumes = {
MediaType.MULTIPART_FORM_DATA_VALUE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE
},
value = "/cert-sign")
@Operation( @Operation(
summary = "Sign PDF with a Digital Certificate", summary = "Sign PDF with a Digital Certificate",
description = description =
@ -137,12 +157,13 @@ public class CertSignController {
MultipartFile p12File = request.getP12File(); MultipartFile p12File = request.getP12File();
MultipartFile jksfile = request.getJksFile(); MultipartFile jksfile = request.getJksFile();
String password = request.getPassword(); String password = request.getPassword();
Boolean showSignature = request.isShowSignature(); Boolean showSignature = request.getShowSignature();
String reason = request.getReason(); String reason = request.getReason();
String location = request.getLocation(); String location = request.getLocation();
String name = request.getName(); String name = request.getName();
Integer pageNumber = request.getPageNumber() - 1; // Convert 1-indexed page number (user input) to 0-indexed page number (API requirement)
Boolean showLogo = request.isShowLogo(); Integer pageNumber = request.getPageNumber() != null ? (request.getPageNumber() - 1) : null;
Boolean showLogo = request.getShowLogo();
if (certType == null) { if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided"); throw new IllegalArgumentException("Cert type must be provided");
@ -279,7 +300,7 @@ public class CertSignController {
widget.setAppearance(appearance); widget.setAppearance(appearance);
try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) { try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) {
if (showLogo) { if (Boolean.TRUE.equals(showLogo)) {
cs.saveGraphicsState(); cs.saveGraphicsState();
PDExtendedGraphicsState extState = new PDExtendedGraphicsState(); PDExtendedGraphicsState extState = new PDExtendedGraphicsState();
extState.setBlendMode(BlendMode.MULTIPLY); extState.setBlendMode(BlendMode.MULTIPLY);

View File

@ -82,6 +82,8 @@ public class ApplicationProperties {
private Metrics metrics = new Metrics(); private Metrics metrics = new Metrics();
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated(); private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private Mail mail = new Mail();
private Premium premium = new Premium(); private Premium premium = new Premium();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition(); private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline(); private AutoPipeline autoPipeline = new AutoPipeline();
@ -420,6 +422,16 @@ public class ApplicationProperties {
} }
} }
@Data
public static class Mail {
private boolean enabled;
private String host;
private int port;
private String username;
@ToString.Exclude private String password;
private String from;
}
@Data @Data
public static class Premium { public static class Premium {
private boolean enabled; private boolean enabled;

View File

@ -0,0 +1,39 @@
package stirling.software.SPDF.model.api;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@ConditionalOnProperty(value = "mail.enabled", havingValue = "true", matchIfMissing = false)
public class Email extends GeneralFile {
@Schema(
description = "The recipient's email address",
requiredMode = Schema.RequiredMode.REQUIRED,
format = "email")
private String to;
@Schema(
description = "The subject of the email",
defaultValue = "Stirling Software PDF Notification",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String subject;
@Schema(
description = "The body of the email",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
defaultValue =
"This message was automatically generated by Stirling-PDF, an innovative"
+ " solution from Stirling Software. For more information, visit our <a"
+ " href=\"https://stirling-software.com\">website</a>.<br><br>Please do"
+ " not reply directly to this email.")
private String body;
}

View File

@ -20,7 +20,8 @@ public class SignPDFWithCertRequest extends PDFFile {
@Schema( @Schema(
description = description =
"The private key for the digital certificate (required for PEM type certificates)") "The private key for the digital certificate (required for PEM type"
+ " certificates)")
private MultipartFile privateKeyFile; private MultipartFile privateKeyFile;
@Schema(description = "The digital certificate (required for PEM type certificates)") @Schema(description = "The digital certificate (required for PEM type certificates)")
@ -32,11 +33,11 @@ public class SignPDFWithCertRequest extends PDFFile {
@Schema(description = "The JKS keystore file (Java Key Store)") @Schema(description = "The JKS keystore file (Java Key Store)")
private MultipartFile jksFile; private MultipartFile jksFile;
@Schema(description = "The password for the keystore or the private key") @Schema(description = "The password for the keystore or the private key", format = "password")
private String password; private String password;
@Schema(description = "Whether to visually show the signature in the PDF file") @Schema(description = "Whether to visually show the signature in the PDF file")
private boolean showSignature; private Boolean showSignature;
@Schema(description = "The reason for signing the PDF") @Schema(description = "The reason for signing the PDF")
private String reason; private String reason;
@ -49,9 +50,10 @@ public class SignPDFWithCertRequest extends PDFFile {
@Schema( @Schema(
description = description =
"The page number where the signature should be visible. This is required if showSignature is set to true") "The page number where the signature should be visible. This is required if"
+ " showSignature is set to true")
private Integer pageNumber; private Integer pageNumber;
@Schema(description = "Whether to visually show a signature logo along with the signature") @Schema(description = "Whether to visually show a signature logo along with the signature")
private boolean showLogo; private Boolean showLogo;
} }

View File

@ -16,7 +16,7 @@ import stirling.software.SPDF.utils.misc.ReplaceAndInvertColorStrategy;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ReplaceAndInvertColorService { public class ReplaceAndInvertColorService {
private ReplaceAndInvertColorFactory replaceAndInvertColorFactory; private final ReplaceAndInvertColorFactory replaceAndInvertColorFactory;
public InputStreamResource replaceAndInvertColor( public InputStreamResource replaceAndInvertColor(
MultipartFile file, MultipartFile file,

View File

@ -76,6 +76,14 @@ premium:
apiKey: '' apiKey: ''
appId: '' appId: ''
mail:
enabled: true # set to 'true' to enable sending emails
host: smtp.example.com # SMTP server hostname
port: 587 # SMTP server port
username: '' # SMTP server username
password: '' # SMTP server password
from: '' # sender email address
legal: legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder

View File

@ -270,7 +270,7 @@
{ {
"moduleName": "com.opencsv:opencsv", "moduleName": "com.opencsv:opencsv",
"moduleUrl": "http://opencsv.sf.net", "moduleUrl": "http://opencsv.sf.net",
"moduleVersion": "5.10", "moduleVersion": "5.11",
"moduleLicense": "Apache 2", "moduleLicense": "Apache 2",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@ -623,21 +623,21 @@
{ {
"moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations",
"moduleVersion": "2.2.29", "moduleVersion": "2.2.30",
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{ {
"moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleName": "io.swagger.core.v3:swagger-core-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core",
"moduleVersion": "2.2.29", "moduleVersion": "2.2.30",
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{ {
"moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleName": "io.swagger.core.v3:swagger-models-jakarta",
"moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models",
"moduleVersion": "2.2.29", "moduleVersion": "2.2.30",
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
@ -871,7 +871,7 @@
{ {
"moduleName": "org.apache.pdfbox:fontbox", "moduleName": "org.apache.pdfbox:fontbox",
"moduleUrl": "https://pdfbox.apache.org", "moduleUrl": "https://pdfbox.apache.org",
"moduleVersion": "3.0.4", "moduleVersion": "3.0.5",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@ -884,28 +884,28 @@
{ {
"moduleName": "org.apache.pdfbox:pdfbox", "moduleName": "org.apache.pdfbox:pdfbox",
"moduleUrl": "https://pdfbox.apache.org", "moduleUrl": "https://pdfbox.apache.org",
"moduleVersion": "3.0.4", "moduleVersion": "3.0.5",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{ {
"moduleName": "org.apache.pdfbox:pdfbox-io", "moduleName": "org.apache.pdfbox:pdfbox-io",
"moduleUrl": "https://pdfbox.apache.org", "moduleUrl": "https://pdfbox.apache.org",
"moduleVersion": "3.0.4", "moduleVersion": "3.0.5",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{ {
"moduleName": "org.apache.pdfbox:preflight", "moduleName": "org.apache.pdfbox:preflight",
"moduleUrl": "https://pdfbox.apache.org", "moduleUrl": "https://pdfbox.apache.org",
"moduleVersion": "3.0.4", "moduleVersion": "3.0.5",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{ {
"moduleName": "org.apache.pdfbox:xmpbox", "moduleName": "org.apache.pdfbox:xmpbox",
"moduleUrl": "https://pdfbox.apache.org", "moduleUrl": "https://pdfbox.apache.org",
"moduleVersion": "3.0.4", "moduleVersion": "3.0.5",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@ -1011,6 +1011,13 @@
"moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception", "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception",
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html" "moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
}, },
{
"moduleName": "org.eclipse.angus:jakarta.mail",
"moduleUrl": "https://www.eclipse.org",
"moduleVersion": "2.0.3",
"moduleLicense": "GPL2 w/ CPE",
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
},
{ {
"moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client", "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client",
"moduleUrl": "https://jetty.org/", "moduleUrl": "https://jetty.org/",
@ -1235,6 +1242,12 @@
"moduleLicense": "GNU Library General Public License v2.1 or later", "moduleLicense": "GNU Library General Public License v2.1 or later",
"moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1" "moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1"
}, },
{
"moduleName": "org.hibernate.validator:hibernate-validator",
"moduleVersion": "8.0.2.Final",
"moduleLicense": "Apache License 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{ {
"moduleName": "org.jboss.logging:jboss-logging", "moduleName": "org.jboss.logging:jboss-logging",
"moduleUrl": "http://www.jboss.org", "moduleUrl": "http://www.jboss.org",
@ -1423,19 +1436,19 @@
}, },
{ {
"moduleName": "org.springdoc:springdoc-openapi-starter-common", "moduleName": "org.springdoc:springdoc-openapi-starter-common",
"moduleVersion": "2.8.6", "moduleVersion": "2.8.8",
"moduleLicense": "The Apache License, Version 2.0", "moduleLicense": "The Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{ {
"moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-api", "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-api",
"moduleVersion": "2.8.6", "moduleVersion": "2.8.8",
"moduleLicense": "The Apache License, Version 2.0", "moduleLicense": "The Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
{ {
"moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-ui", "moduleName": "org.springdoc:springdoc-openapi-starter-webmvc-ui",
"moduleVersion": "2.8.6", "moduleVersion": "2.8.8",
"moduleLicense": "The Apache License, Version 2.0", "moduleLicense": "The Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@ -1523,6 +1536,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "org.springframework.boot:spring-boot-starter-mail",
"moduleUrl": "https://spring.io/projects/spring-boot",
"moduleVersion": "3.4.5",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client", "moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client",
"moduleUrl": "https://spring.io/projects/spring-boot", "moduleUrl": "https://spring.io/projects/spring-boot",
@ -1544,6 +1564,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "org.springframework.boot:spring-boot-starter-validation",
"moduleUrl": "https://spring.io/projects/spring-boot",
"moduleVersion": "3.4.5",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "org.springframework.boot:spring-boot-starter-web", "moduleName": "org.springframework.boot:spring-boot-starter-web",
"moduleUrl": "https://spring.io/projects/spring-boot", "moduleUrl": "https://spring.io/projects/spring-boot",
@ -1656,6 +1683,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "org.springframework:spring-context-support",
"moduleUrl": "https://github.com/spring-projects/spring-framework",
"moduleVersion": "6.2.6",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "org.springframework:spring-core", "moduleName": "org.springframework:spring-core",
"moduleUrl": "https://github.com/spring-projects/spring-framework", "moduleUrl": "https://github.com/spring-projects/spring-framework",
@ -1746,7 +1780,7 @@
{ {
"moduleName": "org.webjars:swagger-ui", "moduleName": "org.webjars:swagger-ui",
"moduleUrl": "https://www.webjars.org", "moduleUrl": "https://www.webjars.org",
"moduleVersion": "5.20.1", "moduleVersion": "5.21.0",
"moduleLicense": "Apache-2.0" "moduleLicense": "Apache-2.0"
}, },
{ {

View File

@ -13,24 +13,24 @@
<div th:replace="~{fragments/languageEntry :: languageEntry ('eu_ES', 'Euskara')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('eu_ES', 'Euskara')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('es_ES', 'Español')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('es_ES', 'Español')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('fr_FR', 'Français')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('fr_FR', 'Français')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('id_ID', 'Indonesia')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('id_ID', 'Bahasa Indonesia')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ga_IE', 'Irish')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ga_IE', 'Gaeilge')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('it_IT', 'Italiano')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('it_IT', 'Italiano')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('nl_NL', 'Nederlands')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('nl_NL', 'Nederlands')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('fa_IR', 'پارسی')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('fa_IR', 'پارسی')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('pl_PL', 'Polski')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_BR', 'Português (BR)')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('pt_BR', 'Português (BR)')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('pt_PT', 'Português (PT)')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('pt_PT', 'Português (PT)')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ro_RO', 'Romanian')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ro_RO', 'Română')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sk_SK', 'Slovensky')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('sk_SK', 'Slovenčina')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sl_SI', 'Slovenian')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('sl_SI', 'Slovenščina')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sv_SE', 'Svenska')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('sv_SE', 'Svenska')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('tr_TR', 'Türkçe')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('tr_TR', 'Türkçe')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ru_RU', 'Русский')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ru_RU', 'Русский')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ko_KR', '한국어')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ko_KR', '한국어')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('ja_JP', '日本語')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('ja_JP', '日本語')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('el_GR', 'Ελληνικά')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('el_GR', 'Ελληνικά')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('hu_HU', 'Hungarian')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('hu_HU', 'Magyar')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('hi_IN', 'हिन्दी')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('hi_IN', 'हिन्दी')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('sr_LATN_RS', 'Srpski')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('sr_LATN_RS', 'Srpski')}" ></div>
<div th:replace="~{fragments/languageEntry :: languageEntry ('uk_UA', 'Українська')}" ></div> <div th:replace="~{fragments/languageEntry :: languageEntry ('uk_UA', 'Українська')}" ></div>

View File

@ -0,0 +1,64 @@
package stirling.software.SPDF.config.security.mail;
import static org.mockito.Mockito.*;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.Email;
@ExtendWith(MockitoExtension.class)
public class EmailServiceTest {
@Mock
private JavaMailSender mailSender;
@Mock
private ApplicationProperties applicationProperties;
@Mock
private ApplicationProperties.Mail mailProperties;
@Mock
private MultipartFile fileInput;
@InjectMocks
private EmailService emailService;
@Test
void testSendEmailWithAttachment() throws MessagingException {
// Mock the values returned by ApplicationProperties
when(applicationProperties.getMail()).thenReturn(mailProperties);
when(mailProperties.getFrom()).thenReturn("no-reply@stirling-software.com");
// Create a mock Email object
Email email = new Email();
email.setTo("test@example.com");
email.setSubject("Test Email");
email.setBody("This is a test email.");
email.setFileInput(fileInput);
// Mock MultipartFile behavior
when(fileInput.getOriginalFilename()).thenReturn("testFile.txt");
// Mock MimeMessage
MimeMessage mimeMessage = mock(MimeMessage.class);
// Configure mailSender to return the mocked MimeMessage
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
// Call the service method
emailService.sendEmailWithAttachment(email);
// Verify that the email was sent using mailSender
verify(mailSender).send(mimeMessage);
}
}

View File

@ -0,0 +1,84 @@
package stirling.software.SPDF.controller.api;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import jakarta.mail.MessagingException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.config.security.mail.EmailService;
import stirling.software.SPDF.model.api.Email;
@ExtendWith(MockitoExtension.class)
public class EmailControllerTest {
private MockMvc mockMvc;
@Mock private EmailService emailService;
@InjectMocks private EmailController emailController;
@Mock private MultipartFile fileInput;
@BeforeEach
void setUp() {
// Set up the MockMvc instance for testing
mockMvc = MockMvcBuilders.standaloneSetup(emailController).build();
}
@Test
void testSendEmailWithAttachmentSuccess() throws Exception {
// Create a mock Email object
Email email = new Email();
email.setTo("test@example.com");
email.setSubject("Test Email");
email.setBody("This is a test email.");
email.setFileInput(fileInput);
// Mock the service to not throw any exception
doNothing().when(emailService).sendEmailWithAttachment(any(Email.class));
// Perform the request and verify the response
mockMvc.perform(
multipart("/api/v1/general/send-email")
.file("fileInput", "dummy-content".getBytes())
.param("to", email.getTo())
.param("subject", email.getSubject())
.param("body", email.getBody()))
.andExpect(status().isOk())
.andExpect(content().string("Email sent successfully"));
}
@Test
void testSendEmailWithAttachmentFailure() throws Exception {
// Create a mock Email object
Email email = new Email();
email.setTo("test@example.com");
email.setSubject("Test Email");
email.setBody("This is a test email.");
email.setFileInput(fileInput);
// Mock the service to throw a MessagingException
doThrow(new MessagingException("Failed to send email"))
.when(emailService)
.sendEmailWithAttachment(any(Email.class));
// Perform the request and verify the response
mockMvc.perform(
multipart("/api/v1/general/send-email")
.file("fileInput", "dummy-content".getBytes())
.param("to", email.getTo())
.param("subject", email.getSubject())
.param("body", email.getBody()))
.andExpect(status().isInternalServerError())
.andExpect(content().string("Failed to send email: Failed to send email"));
}
}