Fix cert-sign API NullPointerException when pageNumber is omitted for invisible signatures (#3463)

# Description of Changes

Please provide a summary of the changes, including:

- **What was changed**  
- Updated `SignPDFWithCertRequest` to use `Boolean` for `showSignature`
and `showLogo`, and made `pageNumber` nullable.
  - In `CertSignController`:
- Added an `@InitBinder` to convert empty multipart fields to `null`.
- Extended `@PostMapping` to consume both `multipart/form-data` and
`application/x-www-form-urlencoded`.
- Wrapped `pageNumber` calculation in a null-check (`pageNumber =
request.getPageNumber() != null ? request.getPageNumber() - 1 : null`).
- Changed signature-visualization and logo checks to
`Boolean.TRUE.equals(...)` to avoid unboxing NPE.
  - Cleaned up imports and schema annotations in the request model.

- **Why the change was made**  
- Prevent a 500 Internal Server Error caused by calling `.intValue()` on
a null `pageNumber` when `showSignature=false` (invisible signatures).
- Ensure that omitting `pageNumber` doesn’t break clients using the “try
it out” swagger UI or `curl`-based requests.

- **Any challenges encountered**  
- Configuring Spring’s data binder to treat empty file inputs as `null`
required a custom `PropertyEditorSupport`.
- Balancing backward compatibility with stricter type handling
(switching from primitive `boolean` to boxed `Boolean`).

Closes #3459

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
Ludy 2025-05-07 10:19:06 +02:00 committed by GitHub
parent e2a5874a88
commit 2ac606608a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 37 additions and 14 deletions

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,13 +136,18 @@ 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 =
"This endpoint accepts a PDF file, a digital certificate and related" "This endpoint accepts a PDF file, a digital certificate and related"
+ " information to sign the PDF. It then returns the digitally signed PDF" + " information to sign the PDF. It then returns the digitally signed PDF"
+ " file. Input:PDF Output:PDF Type:SISO") + " file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception { throws Exception {
MultipartFile pdf = request.getFileInput(); MultipartFile pdf = request.getFileInput();
@ -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

@ -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;
} }