From 2ac606608aa545f83c78b8baf95626cdf18c2536 Mon Sep 17 00:00:00 2001 From: Ludy Date: Wed, 7 May 2025 10:19:06 +0200 Subject: [PATCH] Fix cert-sign API NullPointerException when pageNumber is omitted for invisible signatures (#3463) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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. --- .../api/security/CertSignController.java | 39 ++++++++++++++----- .../api/security/SignPDFWithCertRequest.java | 12 +++--- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index 7e1f2601f..58e5b848c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.controller.api.security; import java.awt.*; +import java.beans.PropertyEditorSupport; import java.io.*; import java.nio.file.Files; import java.security.*; @@ -53,7 +54,10 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; import org.bouncycastle.pkcs.PKCSException; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; 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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -82,6 +86,18 @@ public class CertSignController { 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 static void sign( @@ -103,8 +119,7 @@ public class CertSignController { signature.setLocation(location); signature.setReason(reason); signature.setSignDate(Calendar.getInstance()); - - if (showSignature) { + if (Boolean.TRUE.equals(showSignature)) { SignatureOptions signatureOptions = new SignatureOptions(); signatureOptions.setVisualSignature( 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( 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") + + " information to sign the PDF. It then returns the digitally signed PDF" + + " file. Input:PDF Output:PDF Type:SISO") public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { MultipartFile pdf = request.getFileInput(); @@ -137,12 +157,13 @@ public class CertSignController { MultipartFile p12File = request.getP12File(); MultipartFile jksfile = request.getJksFile(); String password = request.getPassword(); - Boolean showSignature = request.isShowSignature(); + Boolean showSignature = request.getShowSignature(); String reason = request.getReason(); String location = request.getLocation(); String name = request.getName(); - Integer pageNumber = request.getPageNumber() - 1; - Boolean showLogo = request.isShowLogo(); + // Convert 1-indexed page number (user input) to 0-indexed page number (API requirement) + Integer pageNumber = request.getPageNumber() != null ? (request.getPageNumber() - 1) : null; + Boolean showLogo = request.getShowLogo(); if (certType == null) { throw new IllegalArgumentException("Cert type must be provided"); @@ -279,7 +300,7 @@ public class CertSignController { widget.setAppearance(appearance); try (PDPageContentStream cs = new PDPageContentStream(doc, appearanceStream)) { - if (showLogo) { + if (Boolean.TRUE.equals(showLogo)) { cs.saveGraphicsState(); PDExtendedGraphicsState extState = new PDExtendedGraphicsState(); extState.setBlendMode(BlendMode.MULTIPLY); diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java index b0266f307..d9fe92def 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java @@ -20,7 +20,8 @@ public class SignPDFWithCertRequest extends PDFFile { @Schema( 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; @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)") 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; @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") private String reason; @@ -49,9 +50,10 @@ public class SignPDFWithCertRequest extends PDFFile { @Schema( 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; @Schema(description = "Whether to visually show a signature logo along with the signature") - private boolean showLogo; + private Boolean showLogo; }