From a7cd6bfd2eee4df960fdbbac024199f233d4553c Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 2 Sep 2023 00:05:50 +0100 Subject: [PATCH 01/14] itext changes --- build.gradle | 1 + .../software/SPDF/SPdfApplication.java | 1 - .../software/SPDF/config/OpenApiConfig.java | 9 +- .../config/security/InitialSecuritySetup.java | 15 +- .../api/ToSinglePageController.java | 51 +- .../api/other/PageNumbersController.java | 95 ++- .../controller/api/other/ShowJavascript.java | 76 +-- .../api/security/CertSignController2.java | 258 ++++++++ .../controller/api/security/GetInfoOnPDF.java | 610 +++++++++--------- .../SPDF/model/ApplicationProperties.java | 42 +- .../software/SPDF/utils/GeneralUtils.java | 3 +- src/main/resources/settings.yml.template | 4 - 12 files changed, 668 insertions(+), 497 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java diff --git a/build.gradle b/build.gradle index c8cb5d3a..33d9e069 100644 --- a/build.gradle +++ b/build.gradle @@ -86,6 +86,7 @@ dependencies { //general PDF implementation 'org.apache.pdfbox:pdfbox:2.0.29' + implementation 'org.apache.pdfbox:xmpbox:2.0.29' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' implementation 'com.itextpdf:itext7-core:7.2.5' diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index c26999d0..65542e17 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -66,7 +66,6 @@ public class SPdfApplication { GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/templates/"); - GeneralUtils.createDir("config"); diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 573873da..d3cade1a 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -18,14 +18,9 @@ public class OpenApiConfig { public OpenAPI customOpenAPI() { String version = getClass().getPackage().getImplementationVersion(); if (version == null) { - Properties props = new Properties(); - try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { - props.load(input); - version = props.getProperty("version"); - } catch (IOException ex) { - ex.printStackTrace(); + version = "1.0.0"; // default version if all else fails - } + } return new OpenAPI().components(new Components()).info( diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 08b939b7..7bb13535 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -14,25 +14,26 @@ import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; - +import stirling.software.SPDF.model.*; @Component public class InitialSecuritySetup { @Autowired private UserService userService; + @Autowired ApplicationProperties applicationProperties; @PostConstruct public void init() { if (!userService.hasUsers()) { - String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); - String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); - if (initialUsername != null && initialPassword != null) { - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - } - + String initialUsername = "admin"; + String initialPassword = "stirling"; + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + + + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 42e18e24..4e4ea33f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,8 +1,14 @@ package stirling.software.SPDF.controller.api; +import java.awt.geom.AffineTransform; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -24,6 +30,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class ToSinglePageController { @@ -41,37 +49,34 @@ public class ToSinglePageController { @Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true) MultipartFile file) throws IOException { - PdfReader reader = new PdfReader(file.getInputStream()); - PdfDocument sourceDocument = new PdfDocument(reader); - + PDDocument sourceDocument = PDDocument.load(file.getInputStream()); float totalHeight = 0; float width = 0; - for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) { - Rectangle pageSize = sourceDocument.getPage(i).getPageSize(); + for (PDPage page : sourceDocument.getPages()) { + PDRectangle pageSize = page.getMediaBox(); totalHeight += pageSize.getHeight(); if(width < pageSize.getWidth()) - width = pageSize.getWidth(); + width = pageSize.getWidth(); + } + + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(new PDRectangle(width, totalHeight)); + newDocument.addPage(newPage); + + LayerUtility layerUtility = new LayerUtility(newDocument); + float yOffset = totalHeight; + + for (PDPage page : sourceDocument.getPages()) { + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); + AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); + layerUtility.appendFormAsLayer(newDocument.getPage(0), form, af, page.getResources().getCOSObject().toString()); + yOffset -= page.getMediaBox().getHeight(); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument newDocument = new PdfDocument(writer); - PageSize newPageSize = new PageSize(width, totalHeight); - newDocument.addNewPage(newPageSize); - - Document layoutDoc = new Document(newDocument); - float yOffset = totalHeight; - - for (int i = 1; i <= sourceDocument.getNumberOfPages(); i++) { - PdfFormXObject pageCopy = sourceDocument.getPage(i).copyAsFormXObject(newDocument); - Image copiedPage = new Image(pageCopy); - copiedPage.setFixedPosition(0, yOffset - sourceDocument.getPage(i).getPageSize().getHeight()); - yOffset -= sourceDocument.getPage(i).getPageSize().getHeight(); - layoutDoc.add(copiedPage); - } - - layoutDoc.close(); + newDocument.save(baos); + newDocument.close(); sourceDocument.close(); byte[] result = baos.toByteArray(); diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index 14288c83..c48a77e9 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java @@ -6,6 +6,11 @@ import java.io.IOException; import java.net.URLEncoder; import java.util.List; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; @@ -15,18 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.io.font.constants.StandardFonts; -import com.itextpdf.kernel.font.PdfFont; -import com.itextpdf.kernel.font.PdfFontFactory; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.canvas.PdfCanvas; -import com.itextpdf.layout.Canvas; -import com.itextpdf.layout.element.Paragraph; -import com.itextpdf.layout.properties.TextAlignment; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -51,11 +45,10 @@ public class PageNumbersController { @Parameter(description = "Which pages to number, default all", required = false, schema = @Schema(type = "string")) @RequestParam(value = "pagesToNumber", required = false) String pagesToNumber, @Parameter(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"", required = false, schema = @Schema(type = "string")) @RequestParam(value = "customText", required = false) String customText) throws IOException { + int pageNumber = startingNumber; + byte[] fileBytes = file.getBytes(); + PDDocument document = PDDocument.load(fileBytes); - byte[] fileBytes = file.getBytes(); - ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes); - - int pageNumber = startingNumber; float marginFactor; switch (customMargin.toLowerCase()) { case "small": @@ -67,79 +60,67 @@ public class PageNumbersController { case "large": marginFactor = 0.05f; break; - case "x-large": - marginFactor = 0.1f; - break; default: marginFactor = 0.035f; break; } float fontSize = 12.0f; + PDType1Font font = PDType1Font.HELVETICA; - PdfReader reader = new PdfReader(bais); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - - PdfDocument pdfDoc = new PdfDocument(reader, writer); - - List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), pdfDoc.getNumberOfPages()); + List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); for (int i : pagesToNumberList) { - PdfPage page = pdfDoc.getPage(i+1); - Rectangle pageSize = page.getPageSize(); - PdfCanvas pdfCanvas = new PdfCanvas(page.newContentStreamAfter(), page.getResources(), pdfDoc); + PDPage page = document.getPage(i); + PDRectangle pageSize = page.getMediaBox(); - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(pdfDoc.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); - - PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA); - float textWidth = font.getWidth(text, fontSize); - float textHeight = font.getAscent(text, fontSize) - font.getDescent(text, fontSize); + String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); float x, y; - TextAlignment alignment; int xGroup = (position - 1) % 3; int yGroup = 2 - (position - 1) / 3; switch (xGroup) { case 0: // left - x = pageSize.getLeft() + marginFactor * pageSize.getWidth(); - alignment = TextAlignment.LEFT; + x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); break; case 1: // center - x = pageSize.getLeft() + (pageSize.getWidth()) / 2; - alignment = TextAlignment.CENTER; + x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); break; default: // right - x = pageSize.getRight() - marginFactor * pageSize.getWidth(); - alignment = TextAlignment.RIGHT; + x = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth(); break; } switch (yGroup) { - case 0: // bottom - y = pageSize.getBottom() + marginFactor * pageSize.getHeight(); - break; - case 1: // middle - y = pageSize.getBottom() + (pageSize.getHeight() ) / 2; - break; - default: // top - y = pageSize.getTop() - marginFactor * pageSize.getHeight(); - break; - } + case 0: // bottom + y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); + break; + case 1: // middle + y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); + break; + default: // top + y = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight(); + break; + } - new Canvas(pdfCanvas, page.getPageSize()) - .showTextAligned(new Paragraph(text).setFont(font).setFontSize(fontSize), x, y, alignment); + PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + contentStream.beginText(); + contentStream.setFont(font, fontSize); + contentStream.newLineAtOffset(x, y); + contentStream.showText(text); + contentStream.endText(); + contentStream.close(); pageNumber++; } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + document.close(); - pdfDoc.close(); - byte[] resultBytes = baos.toByteArray(); - - return WebResponseUtils.bytesToWebResponse(resultBytes, URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF); + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java index 122a1871..e8f33144 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java @@ -1,7 +1,11 @@ package stirling.software.SPDF.controller.api.other; import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDNameTreeNode; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -10,16 +14,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.pdf.PdfArray; -import com.itextpdf.kernel.pdf.PdfDictionary; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfName; -import com.itextpdf.kernel.pdf.PdfObject; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfStream; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @@ -28,55 +22,33 @@ public class ShowJavascript { private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); @PostMapping(consumes = "multipart/form-data", value = "/show-javascript") - @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") public ResponseEntity extractHeader( - @RequestPart(value = "fileInput") @Parameter(description = "The input PDF file from which the javascript is to be extracted.", required = true) MultipartFile inputFile) - throws Exception { + @RequestPart(value = "fileInput") MultipartFile inputFile) throws Exception { + + String script = ""; - try ( - PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream())) - ) { - - String name = ""; - String script = ""; - String entryName = "File: "+inputFile.getOriginalFilename() + ", Script: "; - //Javascript - PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); - if (namesDict != null) { - PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript); - if (javascriptDict != null) { + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); - for (int i = 0; i < namesArray.size(); i += 2) { - if(namesArray.getAsString(i) != null) - name = namesArray.getAsString(i).toString(); + PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); - PdfObject jsCode = namesArray.get(i+1); - if (jsCode instanceof PdfStream) { - byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - script = "//" + entryName + name + "\n" +jsCodeStr; + if (jsTree != null) { + Map jsEntries = jsTree.getNames(); - } else if (jsCode instanceof PdfDictionary) { - // If the JS code is in a dictionary, you'll need to know the key to use. - // Assuming the key is PdfName.JS: - PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS); - if (jsCodeStream != null) { - byte[] jsCodeBytes = jsCodeStream.getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - script = "//" + entryName + name + "\n" +jsCodeStr; - } - } - } + for (Map.Entry entry : jsEntries.entrySet()) { + String name = entry.getKey(); + PDActionJavaScript jsAction = entry.getValue(); + String jsCodeStr = jsAction.getAction(); - } + script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; } - if(script.equals("")) { - script = "PDF '" +inputFile.getOriginalFilename() + "' does not contain Javascript"; - } - return WebResponseUtils.bytesToWebResponse(script.getBytes(), name + ".js"); - } - + } + + if (script.isEmpty()) { + script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + } + + return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java new file mode 100644 index 00000000..e159b2a3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java @@ -0,0 +1,258 @@ +package stirling.software.SPDF.controller.api.security; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.util.io.pem.PemReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.cms.CMSTypedData; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ContentDisposition; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.SPDF.utils.WebResponseUtils; + +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +@RestController +@Tag(name = "Security", description = "Security APIs") +public class CertSignController2 { + + private static final Logger logger = LoggerFactory.getLogger(CertSignController2.class); + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + @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:MF-SISO") + public ResponseEntity signPDF( + @RequestPart(required = true, value = "fileInput") + @Parameter(description = "The input PDF file to be signed") + MultipartFile pdf, + + @RequestParam(value = "certType", required = false) + @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) + String certType, + + @RequestParam(value = "key", required = false) + @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") + MultipartFile privateKeyFile, + + @RequestParam(value = "cert", required = false) + @Parameter(description = "The digital certificate (required for PEM type certificates)") + MultipartFile certFile, + + @RequestParam(value = "p12", required = false) + @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") + MultipartFile p12File, + + @RequestParam(value = "password", required = false) + @Parameter(description = "The password for the keystore or the private key") + String password, + + @RequestParam(value = "showSignature", required = false) + @Parameter(description = "Whether to visually show the signature in the PDF file") + Boolean showSignature, + + @RequestParam(value = "reason", required = false) + @Parameter(description = "The reason for signing the PDF") + String reason, + + @RequestParam(value = "location", required = false) + @Parameter(description = "The location where the PDF is signed") + String location, + + @RequestParam(value = "name", required = false) + @Parameter(description = "The name of the signer") + String name, + + @RequestParam(value = "pageNumber", required = false) + @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") + Integer pageNumber) throws Exception { + + BouncyCastleProvider provider = new BouncyCastleProvider(); + Security.addProvider(provider); + + PrivateKey privateKey = null; + X509Certificate cert = null; + + if (certType != null) { + switch (certType) { + case "PKCS12": + if (p12File != null) { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); + String alias = ks.aliases().nextElement(); + privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + cert = (X509Certificate) ks.getCertificate(alias); + } + break; + case "PEM": + if (privateKeyFile != null && certFile != null) { + // Load private key + KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider); + if (isPEM(privateKeyFile.getBytes())) { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); + } else { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); + } + + // Load certificate + CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); + if (isPEM(certFile.getBytes())) { + cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); + } else { + cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); + } + } + break; + } + } + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + + // Load the PDF + try (PDDocument document = PDDocument.load(pdf.getBytes())) { + SignatureOptions signatureOptions = new SignatureOptions(); + + // If you want to show the signature + if (showSignature != null && showSignature) { + // Calculate signature field position based on your requirements + + PDPage page = document.getPage(pageNumber - 1); // zero-based + + PDVisibleSignDesigner signDesigner = new PDVisibleSignDesigner(new ByteArrayInputStream(pdf.getBytes())); + //TODO signDesigner + + PDVisibleSigProperties sigProperties = new PDVisibleSigProperties(); + + //TODO sigProperties extra + signatureOptions.setVisualSignature(sigProperties); + signatureOptions.setPage(pageNumber - 1); + } + + document.addSignature(signature, signatureOptions); + + // External signing + ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); + + byte[] content = IOUtils.toByteArray(externalSigning.getContent()); + + // Using BouncyCastle to sign + CMSTypedData cmsData = new CMSProcessableByteArray(content); + + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider).build(privateKey); + + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(provider).build()).build(signer, cert)); + + gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); + CMSSignedData signedData = gen.generate(cmsData, false); + + byte[] cmsSignature = signedData.getEncoded(); + + externalSigning.setSignature(cmsSignature); + + + // After setting the signature, return the resultant PDF + try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { + document.save(signedPdfOutput); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_PDF); + headers.setContentDisposition(ContentDisposition.builder("attachment").filename("signed.pdf").build()); + + return new ResponseEntity<>(signedPdfOutput.toByteArray(), headers, HttpStatus.OK); + } + } + + + } + + private byte[] parsePEM(byte[] content) throws IOException { + PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + return pemReader.readPemObject().getContent(); + } + + private boolean isPEM(byte[] content) { + String contentStr = new String(content); + return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + } + + + + + +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 393ac7ed..ef4187f7 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -10,14 +10,52 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - +import java.io.ByteArrayOutputStream; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSDocument; +import org.apache.pdfbox.cos.COSInputStream; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSObject; +import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; 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.COSObjectable; +import org.apache.pdfbox.pdmodel.common.PDMetadata; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionJavaScript; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.text.PDFTextStripper; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -25,33 +63,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.xml.XmpParsingException; +import org.apache.xmpbox.xml.XmpSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.itextpdf.forms.PdfAcroForm; -import com.itextpdf.forms.fields.PdfFormField; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfArray; -import com.itextpdf.kernel.pdf.PdfCatalog; -import com.itextpdf.kernel.pdf.PdfDictionary; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfName; -import com.itextpdf.kernel.pdf.PdfObject; -import com.itextpdf.kernel.pdf.PdfOutline; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfResources; -import com.itextpdf.kernel.pdf.PdfStream; -import com.itextpdf.kernel.pdf.PdfString; -import com.itextpdf.kernel.pdf.annot.PdfAnnotation; -import com.itextpdf.kernel.pdf.annot.PdfFileAttachmentAnnotation; -import com.itextpdf.kernel.pdf.annot.PdfLinkAnnotation; -import com.itextpdf.kernel.pdf.layer.PdfLayer; -import com.itextpdf.kernel.pdf.layer.PdfOCProperties; -import com.itextpdf.kernel.xmp.XMPException; -import com.itextpdf.kernel.xmp.XMPMeta; -import com.itextpdf.kernel.xmp.XMPMetaFactory; -import com.itextpdf.kernel.xmp.options.SerializeOptions; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -72,7 +90,6 @@ public class GetInfoOnPDF { try ( PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); - PdfDocument itextDoc = new PdfDocument(new PdfReader(inputFile.getInputStream())) ) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode jsonOutput = objectMapper.createObjectNode(); @@ -120,21 +137,17 @@ public class GetInfoOnPDF { boolean hasCompression = false; String compressionType = "None"; - // Check for object streams - for (int i = 1; i <= itextDoc.getNumberOfPdfObjects(); i++) { - PdfObject obj = itextDoc.getPdfObject(i); - if (obj != null && obj.isStream() && ((PdfStream) obj).get(PdfName.Type) == PdfName.ObjStm) { - hasCompression = true; - compressionType = "Object Streams"; - break; + COSDocument cosDoc = pdfBoxDoc.getDocument(); + for (COSObject cosObject : cosDoc.getObjects()) { + if (cosObject.getObject() instanceof COSStream) { + COSStream cosStream = (COSStream) cosObject.getObject(); + if (COSName.OBJ_STM.equals(cosStream.getItem(COSName.TYPE))) { + hasCompression = true; + compressionType = "Object Streams"; + break; + } } } - - // If not compressed using object streams, check for compressed Xref tables - if (!hasCompression && itextDoc.getReader().hasRebuiltXref()) { - hasCompression = true; - compressionType = "Compressed Xref or Rebuilt Xref"; - } basicInfo.put("Compression", hasCompression); if(hasCompression) basicInfo.put("CompressionType", compressionType); @@ -144,9 +157,8 @@ public class GetInfoOnPDF { basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); - // Page Mode using iText7 - PdfCatalog catalog = itextDoc.getCatalog(); - PdfName pageMode = catalog.getPdfObject().getAsName(PdfName.PageMode); + PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); + String pageMode = catalog.getPageMode().name(); // Document Information using PDFBox docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); @@ -157,49 +169,54 @@ public class GetInfoOnPDF { - PdfAcroForm acroForm = PdfAcroForm.getAcroForm(itextDoc, false); + PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); + ObjectNode formFieldsNode = objectMapper.createObjectNode(); if (acroForm != null) { - for (Map.Entry entry : acroForm.getFormFields().entrySet()) { - formFieldsNode.put(entry.getKey(), entry.getValue().getValueAsString()); + for (PDField field : acroForm.getFieldTree()) { + formFieldsNode.put(field.getFullyQualifiedName(), field.getValueAsString()); } } jsonOutput.set("FormFields", formFieldsNode); + //embeed files TODO size + PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); + ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); - if(itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) != null) - { - PdfDictionary embeddedFiles = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names) - .getAsDictionary(PdfName.EmbeddedFiles); - if (embeddedFiles != null) { - - PdfArray namesArray = embeddedFiles.getAsArray(PdfName.Names); - if(namesArray != null) { - for (int i = 0; i < namesArray.size(); i += 2) { - ObjectNode embeddedFileNode = objectMapper.createObjectNode(); - embeddedFileNode.put("Name", namesArray.getAsString(i).toString()); - // Add other details if required - embeddedFilesArray.add(embeddedFileNode); - } + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + for (Map.Entry entry : efMap.entrySet()) { + ObjectNode embeddedFileNode = objectMapper.createObjectNode(); + embeddedFileNode.put("Name", entry.getKey()); + PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); + if (embeddedFile != null) { + embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes + } + embeddedFilesArray.add(embeddedFileNode); + } } - - } } other.set("EmbeddedFiles", embeddedFilesArray); + + //attachments TODO size ArrayNode attachmentsArray = objectMapper.createArrayNode(); - for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { - for (PdfAnnotation annotation : itextDoc.getPage(pageNum).getAnnotations()) { - if (annotation instanceof PdfFileAttachmentAnnotation) { + for (PDPage page : pdfBoxDoc.getPages()) { + for (PDAnnotation annotation : page.getAnnotations()) { + if (annotation instanceof PDAnnotationFileAttachment) { + PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; + ObjectNode attachmentNode = objectMapper.createObjectNode(); - attachmentNode.put("Name", ((PdfFileAttachmentAnnotation) annotation).getName().toString()); - attachmentNode.put("Description", annotation.getContents().getValue()); + attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); + attachmentNode.put("Description", fileAttachmentAnnotation.getContents()); + attachmentsArray.add(attachmentNode); } } @@ -207,65 +224,54 @@ public class GetInfoOnPDF { other.set("Attachments", attachmentsArray); //Javascript - PdfDictionary namesDict = itextDoc.getCatalog().getPdfObject().getAsDictionary(PdfName.Names); + PDDocumentNameDictionary namesDict = catalog.getNames(); ArrayNode javascriptArray = objectMapper.createArrayNode(); + if (namesDict != null) { - PdfDictionary javascriptDict = namesDict.getAsDictionary(PdfName.JavaScript); + PDJavascriptNameTreeNode javascriptDict = namesDict.getJavaScript(); if (javascriptDict != null) { + try { + Map jsEntries = javascriptDict.getNames(); - PdfArray namesArray = javascriptDict.getAsArray(PdfName.Names); - for (int i = 0; i < namesArray.size(); i += 2) { - ObjectNode jsNode = objectMapper.createObjectNode(); - if(namesArray.getAsString(i) != null) - jsNode.put("JS Name", namesArray.getAsString(i).toString()); + for (Map.Entry entry : jsEntries.entrySet()) { + ObjectNode jsNode = objectMapper.createObjectNode(); + jsNode.put("JS Name", entry.getKey()); - // Here we check for a PdfStream object and retrieve the JS code from it - PdfObject jsCode = namesArray.get(i+1); - if (jsCode instanceof PdfStream) { - byte[] jsCodeBytes = ((PdfStream)jsCode).getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - jsNode.put("JS Script Length", jsCodeStr.length()); - } else if (jsCode instanceof PdfDictionary) { - // If the JS code is in a dictionary, you'll need to know the key to use. - // Assuming the key is PdfName.JS: - PdfStream jsCodeStream = ((PdfDictionary)jsCode).getAsStream(PdfName.JS); - if (jsCodeStream != null) { - byte[] jsCodeBytes = jsCodeStream.getBytes(); - String jsCodeStr = new String(jsCodeBytes, StandardCharsets.UTF_8); - jsNode.put("JS Script Character Length", jsCodeStr.length()); + PDActionJavaScript jsAction = entry.getValue(); + if (jsAction != null) { + String jsCodeStr = jsAction.getAction(); + if (jsCodeStr != null) { + jsNode.put("JS Script Length", jsCodeStr.length()); + } } + + javascriptArray.add(jsNode); } - - javascriptArray.add(jsNode); + } catch (IOException e) { + e.printStackTrace(); } - } } other.set("JavaScript", javascriptArray); + //TODO size - PdfOCProperties ocProperties = itextDoc.getCatalog().getOCProperties(false); + PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); ArrayNode layersArray = objectMapper.createArrayNode(); + if (ocProperties != null) { - - for (PdfLayer layer : ocProperties.getLayers()) { + for (PDOptionalContentGroup ocg : ocProperties.getOptionalContentGroups()) { ObjectNode layerNode = objectMapper.createObjectNode(); - layerNode.put("Name", layer.getPdfObject().getAsString(PdfName.Name).toString()); + layerNode.put("Name", ocg.getName()); layersArray.add(layerNode); } - } + other.set("Layers", layersArray); //TODO Security - - - - - // Digital Signatures using iText7 TODO - @@ -282,13 +288,13 @@ public class GetInfoOnPDF { } - boolean isPdfACompliant = checkOutputIntent(itextDoc, "PDF/A"); - boolean isPdfXCompliant = checkOutputIntent(itextDoc, "PDF/X"); - boolean isPdfECompliant = checkForStandard(itextDoc, "PDF/E"); - boolean isPdfVTCompliant = checkForStandard(itextDoc, "PDF/VT"); - boolean isPdfUACompliant = checkForStandard(itextDoc, "PDF/UA"); - boolean isPdfBCompliant = checkForStandard(itextDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. - boolean isPdfSECCompliant = checkForStandard(itextDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. + boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); + boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); + boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); + boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); + boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); + boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. + boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant); @@ -302,27 +308,39 @@ public class GetInfoOnPDF { + PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); ArrayNode bookmarksArray = objectMapper.createArrayNode(); - PdfOutline root = itextDoc.getOutlines(false); + if (root != null) { - for (PdfOutline child : root.getAllChildren()) { + for (PDOutlineItem child : root.children()) { addOutlinesToArray(child, bookmarksArray); } } + other.set("Bookmarks/Outline/TOC", bookmarksArray); - byte[] xmpBytes = itextDoc.getXmpMetadata(); - String xmpString = null; - if (xmpBytes != null) { - try { - XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes); - xmpString = new String(XMPMetaFactory.serializeToBuffer(xmpMeta, new SerializeOptions())); - } catch (XMPException e) { - e.printStackTrace(); - } - } - other.put("XMPMetadata", xmpString); - + + + PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); + + String xmpString = null; + + if (pdMetadata != null) { + try { + COSInputStream is = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(is); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, os, true); + xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); + } catch (XmpParsingException | IOException e) { + e.printStackTrace(); + } + } + + other.put("XMPMetadata", xmpString); + if (pdfBoxDoc.isEncrypted()) { @@ -356,23 +374,40 @@ public class GetInfoOnPDF { ObjectNode pageInfoParent = objectMapper.createObjectNode(); - for (int pageNum = 1; pageNum <= itextDoc.getNumberOfPages(); pageNum++) { + for (int pageNum = 1; pageNum <= pdfBoxDoc.getNumberOfPages(); pageNum++) { ObjectNode pageInfo = objectMapper.createObjectNode(); + // Retrieve the page + PDPage page = pdfBoxDoc.getPage(pageNum); + // Page-level Information - Rectangle pageSize = itextDoc.getPage(pageNum).getPageSize(); - pageInfo.put("Width", pageSize.getWidth()); - pageInfo.put("Height", pageSize.getHeight()); - pageInfo.put("Rotation", itextDoc.getPage(pageNum).getRotation()); - pageInfo.put("Page Orientation", getPageOrientation(pageSize.getWidth(),pageSize.getHeight())); - pageInfo.put("Standard Size", getPageSize(pageSize.getWidth(),pageSize.getHeight())); - + PDRectangle mediaBox = page.getMediaBox(); + + float width = mediaBox.getWidth(); + float height = mediaBox.getHeight(); + + pageInfo.put("Width", width); + pageInfo.put("Height", height); + pageInfo.put("Rotation", page.getRotation()); + + pageInfo.put("Page Orientation", getPageOrientation(width, height)); + pageInfo.put("Standard Size", getPageSize(width, height)); + // Boxes - pageInfo.put("MediaBox", itextDoc.getPage(pageNum).getMediaBox().toString()); - pageInfo.put("CropBox", itextDoc.getPage(pageNum).getCropBox().toString()); - pageInfo.put("BleedBox", itextDoc.getPage(pageNum).getBleedBox().toString()); - pageInfo.put("TrimBox", itextDoc.getPage(pageNum).getTrimBox().toString()); - pageInfo.put("ArtBox", itextDoc.getPage(pageNum).getArtBox().toString()); + pageInfo.put("MediaBox", mediaBox.toString()); + + // Assuming the following boxes are defined for your document; if not, you may get null values. + PDRectangle cropBox = page.getCropBox(); + pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); + + PDRectangle bleedBox = page.getBleedBox(); + pageInfo.put("BleedBox", bleedBox == null ? "Undefined" : bleedBox.toString()); + + PDRectangle trimBox = page.getTrimBox(); + pageInfo.put("TrimBox", trimBox == null ? "Undefined" : trimBox.toString()); + + PDRectangle artBox = page.getArtBox(); + pageInfo.put("ArtBox", artBox == null ? "Undefined" : artBox.toString()); // Content Extraction PDFTextStripper textStripper = new PDFTextStripper(); @@ -382,17 +417,18 @@ public class GetInfoOnPDF { pageInfo.put("Text Characters Count", pageText.length()); // - // Annotations - List annotations = itextDoc.getPage(pageNum).getAnnotations(); + // Annotations + + List annotations = page.getAnnotations(); int subtypeCount = 0; int contentsCount = 0; - for (PdfAnnotation annotation : annotations) { - if(annotation.getSubtype() != null) { + for (PDAnnotation annotation : annotations) { + if (annotation.getSubtype() != null) { subtypeCount++; // Increase subtype count } - if(annotation.getContents() != null) { + if (annotation.getContents() != null) { contentsCount++; // Increase contents count } } @@ -403,25 +439,31 @@ public class GetInfoOnPDF { annotationsObject.put("ContentsCount", contentsCount); pageInfo.set("Annotations", annotationsObject); + + // Images (simplified) // This part is non-trivial as images can be embedded in multiple ways in a PDF. // Here is a basic structure to recognize image XObjects on a page. ArrayNode imagesArray = objectMapper.createArrayNode(); - PdfResources resources = itextDoc.getPage(pageNum).getResources(); - for (PdfName name : resources.getResourceNames()) { - PdfObject obj = resources.getResource(name); - if (obj instanceof PdfStream) { - PdfStream stream = (PdfStream) obj; - if (PdfName.Image.equals(stream.getAsName(PdfName.Subtype))) { - ObjectNode imageNode = objectMapper.createObjectNode(); - imageNode.put("Width", stream.getAsNumber(PdfName.Width).intValue()); - imageNode.put("Height", stream.getAsNumber(PdfName.Height).intValue()); - PdfObject colorSpace = stream.get(PdfName.ColorSpace); - if (colorSpace != null) { - imageNode.put("ColorSpace", colorSpace.toString()); - } - imagesArray.add(imageNode); + PDResources resources = page.getResources(); + + + for (COSName name : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(name); + if (xObject instanceof PDImageXObject) { + PDImageXObject image = (PDImageXObject) xObject; + + ObjectNode imageNode = objectMapper.createObjectNode(); + imageNode.put("Width", image.getWidth()); + imageNode.put("Height", image.getHeight()); + if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { + imageNode.put("Name", image.getMetadata().getFile().getFile()); } + if (image.getColorSpace() != null) { + imageNode.put("ColorSpace", image.getColorSpace().getName()); + } + + imagesArray.add(imageNode); } } pageInfo.set("Images", imagesArray); @@ -431,12 +473,13 @@ public class GetInfoOnPDF { ArrayNode linksArray = objectMapper.createArrayNode(); Set uniqueURIs = new HashSet<>(); // To store unique URIs - for (PdfAnnotation annotation : annotations) { - if (annotation instanceof PdfLinkAnnotation) { - PdfLinkAnnotation linkAnnotation = (PdfLinkAnnotation) annotation; - if(linkAnnotation != null && linkAnnotation.getAction() != null) { - String uri = linkAnnotation.getAction().toString(); - uniqueURIs.add(uri); // Add to set to ensure uniqueness + for (PDAnnotation annotation : annotations) { + if (annotation instanceof PDAnnotationLink) { + PDAnnotationLink linkAnnotation = (PDAnnotationLink) annotation; + if (linkAnnotation.getAction() instanceof PDActionURI) { + PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); + String uri = uriAction.getURI(); + uniqueURIs.add(uri); // Add to set to ensure uniqueness } } } @@ -449,96 +492,52 @@ public class GetInfoOnPDF { } pageInfo.set("Links", linksArray); - // Fonts + + // Fonts ArrayNode fontsArray = objectMapper.createArrayNode(); - PdfDictionary fontDicts = resources.getResource(PdfName.Font); - Set uniqueSubtypes = new HashSet<>(); // To store unique subtypes - - // Map to store unique fonts and their counts Map uniqueFontsMap = new HashMap<>(); - if (fontDicts != null) { - for (PdfName key : fontDicts.keySet()) { - ObjectNode fontNode = objectMapper.createObjectNode(); // Create a new font node for each font - PdfDictionary font = fontDicts.getAsDictionary(key); + for (COSName fontName : resources.getFontNames()) { + PDFont font = resources.getFont(fontName); + ObjectNode fontNode = objectMapper.createObjectNode(); - boolean isEmbedded = font.containsKey(PdfName.FontFile) || - font.containsKey(PdfName.FontFile2) || - font.containsKey(PdfName.FontFile3); - fontNode.put("IsEmbedded", isEmbedded); + fontNode.put("IsEmbedded", font.isEmbedded()); - if (font.containsKey(PdfName.Encoding)) { - String encoding = font.getAsName(PdfName.Encoding).toString(); - fontNode.put("Encoding", encoding); - } + // PDFBox provides Font's BaseFont (i.e., the font name) directly + fontNode.put("Name", font.getName()); - if (font.getAsString(PdfName.BaseFont) != null) { - fontNode.put("Name", font.getAsString(PdfName.BaseFont).toString()); - } + fontNode.put("Subtype", font.getType()); - String subtype = null; - if (font.containsKey(PdfName.Subtype)) { - subtype = font.getAsName(PdfName.Subtype).toString(); - uniqueSubtypes.add(subtype); // Add to set to ensure uniqueness - } - fontNode.put("Subtype", subtype); + PDFontDescriptor fontDescriptor = font.getFontDescriptor(); - PdfDictionary fontDescriptor = font.getAsDictionary(PdfName.FontDescriptor); - if (fontDescriptor != null) { - if (fontDescriptor.containsKey(PdfName.ItalicAngle)) { - fontNode.put("ItalicAngle", fontDescriptor.getAsNumber(PdfName.ItalicAngle).floatValue()); - } + if (fontDescriptor != null) { + fontNode.put("ItalicAngle", fontDescriptor.getItalicAngle()); + int flags = fontDescriptor.getFlags(); + fontNode.put("IsItalic", (flags & 1) != 0); + fontNode.put("IsBold", (flags & 64) != 0); + fontNode.put("IsFixedPitch", (flags & 2) != 0); + fontNode.put("IsSerif", (flags & 4) != 0); + fontNode.put("IsSymbolic", (flags & 8) != 0); + fontNode.put("IsScript", (flags & 16) != 0); + fontNode.put("IsNonsymbolic", (flags & 32) != 0); - if (fontDescriptor.containsKey(PdfName.Flags)) { - int flags = fontDescriptor.getAsNumber(PdfName.Flags).intValue(); - fontNode.put("IsItalic", (flags & 64) != 0); - fontNode.put("IsBold", (flags & 1 << 16) != 0); - fontNode.put("IsFixedPitch", (flags & 1) != 0); - fontNode.put("IsSerif", (flags & 2) != 0); - fontNode.put("IsSymbolic", (flags & 4) != 0); - fontNode.put("IsScript", (flags & 8) != 0); - fontNode.put("IsNonsymbolic", (flags & 16) != 0); - } + fontNode.put("FontFamily", fontDescriptor.getFontFamily()); + // Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity + fontNode.put("FontWeight", fontDescriptor.getFontWeight()); + } - if (fontDescriptor.containsKey(PdfName.FontFamily)) { - String fontFamily = fontDescriptor.getAsString(PdfName.FontFamily).toString(); - fontNode.put("FontFamily", fontFamily); - } - if (fontDescriptor.containsKey(PdfName.FontStretch)) { - String fontStretch = fontDescriptor.getAsName(PdfName.FontStretch).toString(); - fontNode.put("FontStretch", fontStretch); - } + // Create a unique key for this font node based on its attributes + String uniqueKey = fontNode.toString(); - if (fontDescriptor.containsKey(PdfName.FontBBox)) { - PdfArray bbox = fontDescriptor.getAsArray(PdfName.FontBBox); - fontNode.put("FontBoundingBox", bbox.toString()); - } - - if (fontDescriptor.containsKey(PdfName.FontWeight)) { - float fontWeight = fontDescriptor.getAsNumber(PdfName.FontWeight).floatValue(); - fontNode.put("FontWeight", fontWeight); - } - } - - if (font.containsKey(PdfName.ToUnicode)) { - fontNode.put("HasToUnicodeMap", true); - } - - if (fontNode.size() > 0) { - // Create a unique key for this font node based on its attributes - String uniqueKey = fontNode.toString(); - - // Increment count if this font exists, or initialize it if new - if (uniqueFontsMap.containsKey(uniqueKey)) { - ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey); - int count = existingFontNode.get("Count").asInt() + 1; - existingFontNode.put("Count", count); - } else { - fontNode.put("Count", 1); - uniqueFontsMap.put(uniqueKey, fontNode); - } - } + // Increment count if this font exists, or initialize it if new + if (uniqueFontsMap.containsKey(uniqueKey)) { + ObjectNode existingFontNode = uniqueFontsMap.get(uniqueKey); + int count = existingFontNode.get("Count").asInt() + 1; + existingFontNode.put("Count", count); + } else { + fontNode.put("Count", 1); + uniqueFontsMap.put(uniqueKey, fontNode); } } @@ -552,41 +551,49 @@ public class GetInfoOnPDF { - // Access resources dictionary - PdfDictionary resourcesDict = itextDoc.getPage(pageNum).getResources().getPdfObject(); - - // Color Spaces & ICC Profiles + + + + + + + + // Access resources dictionary ArrayNode colorSpacesArray = objectMapper.createArrayNode(); - PdfDictionary colorSpaces = resourcesDict.getAsDictionary(PdfName.ColorSpace); - if (colorSpaces != null) { - for (PdfName name : colorSpaces.keySet()) { - PdfObject colorSpaceObject = colorSpaces.get(name); - if (colorSpaceObject instanceof PdfArray) { - PdfArray colorSpaceArray = (PdfArray) colorSpaceObject; - if (colorSpaceArray.size() > 1 && colorSpaceArray.get(0) instanceof PdfName && PdfName.ICCBased.equals(colorSpaceArray.get(0))) { - ObjectNode iccProfileNode = objectMapper.createObjectNode(); - PdfStream iccStream = (PdfStream) colorSpaceArray.get(1); - byte[] iccData = iccStream.getBytes(); - // TODO: Further decode and analyze the ICC data if needed - iccProfileNode.put("ICC Profile Length", iccData.length); - colorSpacesArray.add(iccProfileNode); - } - } + + Iterable colorSpaceNames = resources.getColorSpaceNames(); + for (COSName name : colorSpaceNames) { + PDColorSpace colorSpace = resources.getColorSpace(name); + if (colorSpace instanceof PDICCBased) { + PDICCBased iccBased = (PDICCBased) colorSpace; + PDStream iccData = iccBased.getPDStream(); + byte[] iccBytes = iccData.toByteArray(); + + // TODO: Further decode and analyze the ICC data if needed + ObjectNode iccProfileNode = objectMapper.createObjectNode(); + iccProfileNode.put("ICC Profile Length", iccBytes.length); + colorSpacesArray.add(iccProfileNode); } } pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); + // Other XObjects Map xObjectCountMap = new HashMap<>(); // To store the count for each type - PdfDictionary xObjects = resourcesDict.getAsDictionary(PdfName.XObject); - if (xObjects != null) { - for (PdfName name : xObjects.keySet()) { - PdfStream xObjectStream = xObjects.getAsStream(name); - String xObjectType = xObjectStream.getAsName(PdfName.Subtype).toString(); - - // Increment the count for this type in the map - xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); + for (COSName name : resources.getXObjectNames()) { + PDXObject xObject = resources.getXObject(name); + String xObjectType; + + if (xObject instanceof PDImageXObject) { + xObjectType = "Image"; + } else if (xObject instanceof PDFormXObject) { + xObjectType = "Form"; + } else { + xObjectType = "Other"; } + + // Increment the count for this type in the map + xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); } // Add the count map to pageInfo (or wherever you want to store it) @@ -598,14 +605,17 @@ public class GetInfoOnPDF { + ArrayNode multimediaArray = objectMapper.createArrayNode(); - for (PdfAnnotation annotation : annotations) { - if (PdfName.RichMedia.equals(annotation.getSubtype())) { + + for (PDAnnotation annotation : annotations) { + if ("RichMedia".equals(annotation.getSubtype())) { ObjectNode multimediaNode = objectMapper.createObjectNode(); - // Extract details from the dictionary as needed + // Extract details from the annotation as needed multimediaArray.add(multimediaNode); } } + pageInfo.set("Multimedia", multimediaArray); @@ -636,17 +646,21 @@ public class GetInfoOnPDF { return null; } - private static void addOutlinesToArray(PdfOutline outline, ArrayNode arrayNode) { + 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); - - for (PdfOutline child : outline.getAllChildren()) { + + 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"; @@ -678,45 +692,33 @@ public class GetInfoOnPDF { return Math.abs(pageAspectRatio - aspectRatio) <= 0.05; } - public boolean checkForStandard(PdfDocument document, String standardKeyword) { - // Check Output Intents - boolean foundInOutputIntents = checkOutputIntent(document, standardKeyword); - if (foundInOutputIntents) return true; - - // Check XMP Metadata (rudimentary) - try { - byte[] metadataBytes = document.getXmpMetadata(); - if (metadataBytes != null) { - XMPMeta xmpMeta = XMPMetaFactory.parseFromBuffer(metadataBytes); - String xmpString = xmpMeta.dumpObject(); - if (xmpString.contains(standardKeyword)) { - return true; - } - } - } catch (XMPException e) { - e.printStackTrace(); - } - - return false; - } - public boolean checkOutputIntent(PdfDocument document, String standard) { - PdfArray outputIntents = document.getCatalog().getPdfObject().getAsArray(PdfName.OutputIntents); - if (outputIntents != null && !outputIntents.isEmpty()) { - for (int i = 0; i < outputIntents.size(); i++) { - PdfDictionary outputIntentDict = outputIntents.getAsDictionary(i); - if (outputIntentDict != null) { - PdfString s = outputIntentDict.getAsString(PdfName.S); - if (s != null && s.toString().contains(standard)) { - return true; - } - } +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; } } - return false; + } catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions. + e.printStackTrace(); } + return false; +} + + public ArrayNode exploreStructureTree(List nodes) { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { @@ -771,7 +773,7 @@ public class GetInfoOnPDF { } } - private String getPageModeDescription(PdfName pageMode) { + private String getPageModeDescription(String pageMode) { return pageMode != null ? pageMode.toString().replaceFirst("/", "") : "Unknown"; } } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index c8cef5a3..45c37efe 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -104,7 +104,6 @@ public class ApplicationProperties { } public static class Security { private Boolean enableLogin; - private InitialLogin initialLogin; private Boolean csrfDisabled; public Boolean getEnableLogin() { @@ -115,14 +114,6 @@ public class ApplicationProperties { this.enableLogin = enableLogin; } - public InitialLogin getInitialLogin() { - return initialLogin != null ? initialLogin : new InitialLogin(); - } - - public void setInitialLogin(InitialLogin initialLogin) { - this.initialLogin = initialLogin; - } - public Boolean getCsrfDisabled() { return csrfDisabled; } @@ -134,40 +125,9 @@ public class ApplicationProperties { @Override public String toString() { - return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" + return "Security [enableLogin=" + enableLogin + ", csrfDisabled=" + csrfDisabled + "]"; } - - - public static class InitialLogin { - - private String username; - private String password; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - @Override - public String toString() { - return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - - - } } public static class System { diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 28a1e73e..34ee9c35 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -65,7 +65,8 @@ public class GeneralUtils { } else if (sizeStr.endsWith("B")) { return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); } else { - // Input string does not have a valid format, handle this case + // Assume MB if no unit is specified + return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); } } catch (NumberFormatException e) { // The numeric part of the input string cannot be parsed, handle this case diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index cecad0dd..4c227b1c 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -4,15 +4,11 @@ security: enableLogin: false # set to 'true' to enable login - initialLogin: - username: 'username' # Specify the initial username for first boot (e.g. 'admin') - password: 'password' # Specify the initial password for first boot (e.g. 'password123') csrfDisabled: true system: defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) googlevisibility: false # 'true' to allow Google visibility, 'false' to disallow - rootURIPath: / # Set the application's root URI (e.g. /pdf-app) customStaticFilePath: '/customFiles/static/' # Directory path for custom static files #ui: From 862086eae525e9e7e33c7d11b131c950bc875870 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:12:08 +0100 Subject: [PATCH 02/14] itext removal fixes --- .../api/ToSinglePageController.java | 66 ++++++++++++------- .../api/other/PageNumbersController.java | 14 +++- .../controller/api/other/ShowJavascript.java | 30 +++++---- .../controller/api/security/GetInfoOnPDF.java | 42 ++++++------ 4 files changed, 91 insertions(+), 61 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 4e4ea33f..2b3c88d4 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,12 +1,15 @@ package stirling.software.SPDF.controller.api; import java.awt.geom.AffineTransform; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.slf4j.Logger; @@ -49,37 +52,50 @@ public class ToSinglePageController { @Parameter(description = "The input multi-page PDF file to be converted into a single page", required = true) MultipartFile file) throws IOException { + // Load the source document PDDocument sourceDocument = PDDocument.load(file.getInputStream()); - float totalHeight = 0; - float width = 0; - for (PDPage page : sourceDocument.getPages()) { - PDRectangle pageSize = page.getMediaBox(); - totalHeight += pageSize.getHeight(); - if(width < pageSize.getWidth()) - width = pageSize.getWidth(); - } + // Calculate total height and max width + float totalHeight = 0; + float maxWidth = 0; + for (PDPage page : sourceDocument.getPages()) { + PDRectangle pageSize = page.getMediaBox(); + totalHeight += pageSize.getHeight(); + maxWidth = Math.max(maxWidth, pageSize.getWidth()); + } - PDDocument newDocument = new PDDocument(); - PDPage newPage = new PDPage(new PDRectangle(width, totalHeight)); - newDocument.addPage(newPage); + // Create new document and page with calculated dimensions + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); + newDocument.addPage(newPage); - LayerUtility layerUtility = new LayerUtility(newDocument); - float yOffset = totalHeight; + // Initialize the content stream of the new page + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + contentStream.close(); + + LayerUtility layerUtility = new LayerUtility(newDocument); + float yOffset = totalHeight; - for (PDPage page : sourceDocument.getPages()) { - PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); - AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); - layerUtility.appendFormAsLayer(newDocument.getPage(0), form, af, page.getResources().getCOSObject().toString()); - yOffset -= page.getMediaBox().getHeight(); - } + // For each page, copy its content to the new page at the correct offset + for (PDPage page : sourceDocument.getPages()) { + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); + AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); + layerUtility.wrapInSaveRestore(newPage); + String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page); + layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); + yOffset -= page.getMediaBox().getHeight(); + } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - newDocument.close(); - sourceDocument.close(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + sourceDocument.close(); - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); + + + + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index c48a77e9..d1a469d0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java @@ -60,6 +60,11 @@ public class PageNumbersController { case "large": marginFactor = 0.05f; break; + case "x-large": + marginFactor = 0.075f; + break; + + default: marginFactor = 0.035f; break; @@ -67,7 +72,12 @@ public class PageNumbersController { float fontSize = 12.0f; PDType1Font font = PDType1Font.HELVETICA; - + if(pagesToNumber == null || pagesToNumber.length() == 0) { + pagesToNumber = "all"; + } + if(customText == null || customText.length() == 0) { + customText = "{n}"; + } List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); for (int i : pagesToNumberList) { @@ -120,7 +130,7 @@ public class PageNumbersController { document.save(baos); document.close(); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), URLEncoder.encode(file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", "UTF-8"), MediaType.APPLICATION_PDF); + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java index e8f33144..d812f5c8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/ShowJavascript.java @@ -28,20 +28,22 @@ public class ShowJavascript { String script = ""; try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - - PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); - - if (jsTree != null) { - Map jsEntries = jsTree.getNames(); - - for (Map.Entry entry : jsEntries.entrySet()) { - String name = entry.getKey(); - PDActionJavaScript jsAction = entry.getValue(); - String jsCodeStr = jsAction.getAction(); - - script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; - } - } + + if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) { + PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); + + if (jsTree != null) { + Map jsEntries = jsTree.getNames(); + + for (Map.Entry entry : jsEntries.entrySet()) { + String name = entry.getKey(); + PDActionJavaScript jsAction = entry.getValue(); + String jsCodeStr = jsAction.getAction(); + + script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; + } + } + } if (script.isEmpty()) { script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index ef4187f7..e75fe502 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -185,24 +185,26 @@ public class GetInfoOnPDF { //embeed files TODO size - PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); - - ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); - if (efTree != null) { - Map efMap = efTree.getNames(); - if (efMap != null) { - for (Map.Entry entry : efMap.entrySet()) { - ObjectNode embeddedFileNode = objectMapper.createObjectNode(); - embeddedFileNode.put("Name", entry.getKey()); - PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); - if (embeddedFile != null) { - embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes - } - embeddedFilesArray.add(embeddedFileNode); - } - } + if(catalog.getNames() != null) { + PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); + + ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + for (Map.Entry entry : efMap.entrySet()) { + ObjectNode embeddedFileNode = objectMapper.createObjectNode(); + embeddedFileNode.put("Name", entry.getKey()); + PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); + if (embeddedFile != null) { + embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes + } + embeddedFilesArray.add(embeddedFileNode); + } + } + } + other.set("EmbeddedFiles", embeddedFilesArray); } - other.set("EmbeddedFiles", embeddedFilesArray); @@ -374,7 +376,7 @@ public class GetInfoOnPDF { ObjectNode pageInfoParent = objectMapper.createObjectNode(); - for (int pageNum = 1; pageNum <= pdfBoxDoc.getNumberOfPages(); pageNum++) { + for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { ObjectNode pageInfo = objectMapper.createObjectNode(); // Retrieve the page @@ -411,8 +413,8 @@ public class GetInfoOnPDF { // Content Extraction PDFTextStripper textStripper = new PDFTextStripper(); - textStripper.setStartPage(pageNum -1); - textStripper.setEndPage(pageNum - 1); + textStripper.setStartPage(pageNum + 1); + textStripper.setEndPage(pageNum +1); String pageText = textStripper.getText(pdfBoxDoc); pageInfo.put("Text Characters Count", pageText.length()); // From ef07963d790e8671826712ec152d09efc170dcde Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:21:55 +0100 Subject: [PATCH 03/14] itext removal --- .../api/MultiPageLayoutController.java | 124 ++++----- .../controller/api/ScalePagesController.java | 240 +++++------------- .../api/ToSinglePageController.java | 10 +- .../templates/other/scale-pages.html | 20 +- 4 files changed, 128 insertions(+), 266 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 9bf0a3d7..13a5e47f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,9 +1,14 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayInputStream; + import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -11,16 +16,10 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.apache.pdfbox.util.Matrix; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.canvas.PdfCanvas; -import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; +import org.apache.pdfbox.multipdf.LayerUtility; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; @@ -34,68 +33,73 @@ public class MultiPageLayoutController { private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") - @Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") + @Operation( + summary = "Merge multiple pages of a PDF document into a single page", + description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO" + ) public ResponseEntity mergeMultiplePagesIntoOne( - @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, - @Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = { - "2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet) - throws IOException { + @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, + @Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = { + "2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet) + throws IOException { - if (pagesPerSheet != 2 && pagesPerSheet != 3 - && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { - throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); - } + if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { + throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); + } - int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); - int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); + int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); + int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); - byte[] bytes = file.getBytes(); - PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); - PdfDocument pdfDoc = new PdfDocument(reader); + PDDocument sourceDocument = PDDocument.load(file.getInputStream()); + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(PDRectangle.A4); + newDocument.addPage(newPage); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument outputPdf = new PdfDocument(writer); - PageSize pageSize = new PageSize(PageSize.A4.rotate()); + int totalPages = sourceDocument.getNumberOfPages(); + float cellWidth = newPage.getMediaBox().getWidth() / cols; + float cellHeight = newPage.getMediaBox().getHeight() / rows; - int totalPages = pdfDoc.getNumberOfPages(); - float cellWidth = pageSize.getWidth() / cols; - float cellHeight = pageSize.getHeight() / rows; + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); - for (int i = 1; i <= totalPages; i += pagesPerSheet) { - PdfPage page = outputPdf.addNewPage(pageSize); - PdfCanvas pdfCanvas = new PdfCanvas(page); + LayerUtility layerUtility = new LayerUtility(newDocument); - for (int row = 0; row < rows; row++) { - for (int col = 0; col < cols; col++) { - int index = i + row * cols + col; - if (index <= totalPages) { - // Get the page and calculate scaling factors - Rectangle rect = pdfDoc.getPage(index).getPageSize(); - float scaleWidth = cellWidth / rect.getWidth(); - float scaleHeight = cellHeight / rect.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); + System.out.println("Reading page " + (i+1)); + PDRectangle rect = sourcePage.getMediaBox(); + float scaleWidth = cellWidth / rect.getWidth(); + float scaleHeight = cellHeight / rect.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight); + System.out.println("Scale for page " + (i+1) + ": " + scale); - PdfFormXObject formXObject = pdfDoc.getPage(index).copyAsFormXObject(outputPdf); - float x = col * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; - float y = (rows - 1 - row) * cellHeight + (cellHeight - rect.getHeight() * scale) / 2; - // Save the graphics state, apply the transformations, add the object, and then - // restore the graphics state - pdfCanvas.saveState(); - pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y); - pdfCanvas.addXObject(formXObject, 0, 0); - pdfCanvas.restoreState(); - } - } - } - } + int rowIndex = i / cols; + int colIndex = i % cols; - outputPdf.close(); - byte[] pdfContent = baos.toByteArray(); - pdfDoc.close(); - - return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); + float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; + float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); + + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); + + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(formXObject); + + contentStream.restoreGraphicsState(); + } + + + contentStream.close(); + sourceDocument.close(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); } + } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index e3b4434a..26ff323f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -11,6 +11,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.util.Matrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -21,19 +27,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.canvas.PdfCanvas; -import com.itextpdf.kernel.pdf.canvas.parser.EventType; -import com.itextpdf.kernel.pdf.canvas.parser.PdfCanvasProcessor; -import com.itextpdf.kernel.pdf.canvas.parser.data.IEventData; -import com.itextpdf.kernel.pdf.canvas.parser.data.TextRenderInfo; -import com.itextpdf.kernel.pdf.canvas.parser.listener.IEventListener; -import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; @@ -41,7 +37,8 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; - +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class ScalePagesController { @@ -55,189 +52,76 @@ public class ScalePagesController { @Parameter(description = "The scale of pages in the output PDF. Acceptable values are A0-A10, B0-B9, LETTER, TABLOID, LEDGER, LEGAL, EXECUTIVE.", required = true, schema = @Schema(type = "string", allowableValues = { "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9", "A10", "B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "LETTER", "TABLOID", "LEDGER", "LEGAL", - "EXECUTIVE" })) @RequestParam("pageSize") String targetPageSize, + "EXECUTIVE" })) @RequestParam("pageSize") String targetPDRectangle, @Parameter(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.", required = true, schema = @Schema(type = "integer")) @RequestParam("scaleFactor") float scaleFactor) throws IOException { - Map sizeMap = new HashMap<>(); + Map sizeMap = new HashMap<>(); // Add A0 - A10 - sizeMap.put("A0", PageSize.A0); - sizeMap.put("A1", PageSize.A1); - sizeMap.put("A2", PageSize.A2); - sizeMap.put("A3", PageSize.A3); - sizeMap.put("A4", PageSize.A4); - sizeMap.put("A5", PageSize.A5); - sizeMap.put("A6", PageSize.A6); - sizeMap.put("A7", PageSize.A7); - sizeMap.put("A8", PageSize.A8); - sizeMap.put("A9", PageSize.A9); - sizeMap.put("A10", PageSize.A10); - // Add B0 - B9 - sizeMap.put("B0", PageSize.B0); - sizeMap.put("B1", PageSize.B1); - sizeMap.put("B2", PageSize.B2); - sizeMap.put("B3", PageSize.B3); - sizeMap.put("B4", PageSize.B4); - sizeMap.put("B5", PageSize.B5); - sizeMap.put("B6", PageSize.B6); - sizeMap.put("B7", PageSize.B7); - sizeMap.put("B8", PageSize.B8); - sizeMap.put("B9", PageSize.B9); + sizeMap.put("A0", PDRectangle.A0); + sizeMap.put("A1", PDRectangle.A1); + sizeMap.put("A2", PDRectangle.A2); + sizeMap.put("A3", PDRectangle.A3); + sizeMap.put("A4", PDRectangle.A4); + sizeMap.put("A5", PDRectangle.A5); + sizeMap.put("A6", PDRectangle.A6); + // Add other sizes - sizeMap.put("LETTER", PageSize.LETTER); - sizeMap.put("TABLOID", PageSize.TABLOID); - sizeMap.put("LEDGER", PageSize.LEDGER); - sizeMap.put("LEGAL", PageSize.LEGAL); - sizeMap.put("EXECUTIVE", PageSize.EXECUTIVE); + sizeMap.put("LETTER", PDRectangle.LETTER); + sizeMap.put("LEGAL", PDRectangle.LEGAL); - if (!sizeMap.containsKey(targetPageSize)) { + if (!sizeMap.containsKey(targetPDRectangle)) { throw new IllegalArgumentException( - "Invalid pageSize. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); + "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); } - PageSize pageSize = sizeMap.get(targetPageSize); + PDRectangle targetSize = sizeMap.get(targetPDRectangle); - byte[] bytes = file.getBytes(); - PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); - PdfDocument pdfDoc = new PdfDocument(reader); + PDDocument sourceDocument = PDDocument.load(file.getBytes()); + PDDocument outputDocument = new PDDocument(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument outputPdf = new PdfDocument(writer); + int totalPages = sourceDocument.getNumberOfPages(); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); + PDRectangle sourceSize = sourcePage.getMediaBox(); + + float scaleWidth = targetSize.getWidth() / sourceSize.getWidth(); + float scaleHeight = targetSize.getHeight() / sourceSize.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; + + PDPage newPage = new PDPage(targetSize); + outputDocument.addPage(newPage); + + PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true); + + float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2; + float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; + + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); + + LayerUtility layerUtility = new LayerUtility(outputDocument); + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(form); - int totalPages = pdfDoc.getNumberOfPages(); + contentStream.restoreGraphicsState(); + contentStream.close(); + } - for (int i = 1; i <= totalPages; i++) { - PdfPage page = outputPdf.addNewPage(pageSize); - PdfCanvas pdfCanvas = new PdfCanvas(page); - // Get the page and calculate scaling factors - Rectangle rect = pdfDoc.getPage(i).getPageSize(); - float scaleWidth = pageSize.getWidth() / rect.getWidth(); - float scaleHeight = pageSize.getHeight() / rect.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; - System.out.println("Scale: " + scale); - PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf); - float x = (pageSize.getWidth() - rect.getWidth() * scale) / 2; // Center Page - float y = (pageSize.getHeight() - rect.getHeight() * scale) / 2; - // Save the graphics state, apply the transformations, add the object, and then - // restore the graphics state - pdfCanvas.saveState(); - pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y); - pdfCanvas.addXObject(formXObject, 0, 0); - pdfCanvas.restoreState(); - } - outputPdf.close(); - byte[] pdfContent = baos.toByteArray(); - pdfDoc.close(); - return WebResponseUtils.bytesToWebResponse(pdfContent, + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + outputDocument.save(baos); + outputDocument.close(); + sourceDocument.close(); + + + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); } - //TODO - @Hidden - @PostMapping(value = "/auto-crop", consumes = "multipart/form-data") - public ResponseEntity cropPdf(@RequestParam("fileInput") MultipartFile file) throws IOException { - byte[] bytes = file.getBytes(); - PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); - PdfDocument pdfDoc = new PdfDocument(reader); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument outputPdf = new PdfDocument(writer); - - int totalPages = pdfDoc.getNumberOfPages(); - for (int i = 1; i <= totalPages; i++) { - PdfPage page = pdfDoc.getPage(i); - Rectangle originalMediaBox = page.getMediaBox(); - - Rectangle contentBox = determineContentBox(page); - - // Make sure we don't go outside the original media box. - Rectangle intersection = originalMediaBox.getIntersection(contentBox); - page.setCropBox(intersection); - - // Copy page to the new document - outputPdf.addPage(page.copyTo(outputPdf)); - } - - outputPdf.close(); - byte[] pdfContent = baos.toByteArray(); - pdfDoc.close(); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" - + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf\"") - .contentType(MediaType.APPLICATION_PDF).body(pdfContent); - } - - private Rectangle determineContentBox(PdfPage page) { - // Extract the text from the page and find the bounding box. - TextBoundingRectangleFinder finder = new TextBoundingRectangleFinder(); - PdfCanvasProcessor processor = new PdfCanvasProcessor(finder); - processor.processPageContent(page); - return finder.getBoundingBox(); - } - - private static class TextBoundingRectangleFinder implements IEventListener { - private List allTextBoxes = new ArrayList<>(); - - public Rectangle getBoundingBox() { - // Sort the text boxes based on their vertical position - allTextBoxes.sort(Comparator.comparingDouble(Rectangle::getTop)); - - // Consider a box an outlier if its top is more than 1.5 times the IQR above the - // third quartile. - int q1Index = allTextBoxes.size() / 4; - int q3Index = 3 * allTextBoxes.size() / 4; - double iqr = allTextBoxes.get(q3Index).getTop() - allTextBoxes.get(q1Index).getTop(); - double threshold = allTextBoxes.get(q3Index).getTop() + 1.5 * iqr; - - // Initialize boundingBox to the first non-outlier box - int i = 0; - while (i < allTextBoxes.size() && allTextBoxes.get(i).getTop() > threshold) { - i++; - } - if (i == allTextBoxes.size()) { - // If all boxes are outliers, just return the first one - return allTextBoxes.get(0); - } - Rectangle boundingBox = allTextBoxes.get(i); - - // Extend the bounding box to include all non-outlier boxes - for (; i < allTextBoxes.size(); i++) { - Rectangle textBoundingBox = allTextBoxes.get(i); - if (textBoundingBox.getTop() > threshold) { - // This box is an outlier, skip it - continue; - } - float left = Math.min(boundingBox.getLeft(), textBoundingBox.getLeft()); - float bottom = Math.min(boundingBox.getBottom(), textBoundingBox.getBottom()); - float right = Math.max(boundingBox.getRight(), textBoundingBox.getRight()); - float top = Math.max(boundingBox.getTop(), textBoundingBox.getTop()); - - // Add a small padding around the bounding box - float padding = 10; - boundingBox = new Rectangle(left - padding, bottom - padding, right - left + 2 * padding, - top - bottom + 2 * padding); - } - return boundingBox; - } - - @Override - public void eventOccurred(IEventData data, EventType type) { - if (type == EventType.RENDER_TEXT) { - TextRenderInfo renderInfo = (TextRenderInfo) data; - allTextBoxes.add(renderInfo.getBaseline().getBoundingRectangle()); - } - } - - @Override - public Set getSupportedEvents() { - return Collections.singleton(EventType.RENDER_TEXT); - } - } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 2b3c88d4..1c35a93f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -11,7 +11,6 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -20,14 +19,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; -import com.itextpdf.layout.Document; -import com.itextpdf.layout.element.Image; + import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/resources/templates/other/scale-pages.html b/src/main/resources/templates/other/scale-pages.html index df0c22ad..c0c4f78e 100644 --- a/src/main/resources/templates/other/scale-pages.html +++ b/src/main/resources/templates/other/scale-pages.html @@ -19,33 +19,15 @@
From 9ece6dacbde6d86fda12898bbefebdc66fc10361 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:32:44 +0100 Subject: [PATCH 04/14] removal --- .../SPDF/controller/api/CropController.java | 110 ++++++++++++------ 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index 09078728..02f40bd1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -4,6 +4,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -25,54 +28,89 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; +import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; +import stirling.software.SPDF.utils.WebResponseUtils; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class CropController { - private static final Logger logger = LoggerFactory.getLogger(CropController.class); - + private static final Logger logger = LoggerFactory.getLogger(CropController.class); @PostMapping(value = "/crop", consumes = "multipart/form-data") @Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") public ResponseEntity cropPdf( - @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, - @Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x, - @Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y, - @Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width, - @Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) throws IOException { - byte[] bytes = file.getBytes(); - System.out.println("x=" + x + ", " + "y=" + y + ", " + "width=" + width + ", " +"height=" + height ); - PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); - PdfDocument pdfDoc = new PdfDocument(reader); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument outputPdf = new PdfDocument(writer); - - int totalPages = pdfDoc.getNumberOfPages(); - - for (int i = 1; i <= totalPages; i++) { - PdfPage page = outputPdf.addNewPage(new PageSize(width, height)); - PdfCanvas pdfCanvas = new PdfCanvas(page); + @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, + @Parameter(description = "The x-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("x") float x, + @Parameter(description = "The y-coordinate of the top-left corner of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("y") float y, + @Parameter(description = "The width of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("width") float width, + @Parameter(description = "The height of the crop area", required = true, schema = @Schema(type = "number")) @RequestParam("height") float height) + throws IOException { - PdfFormXObject formXObject = pdfDoc.getPage(i).copyAsFormXObject(outputPdf); - // Save the graphics state, apply the transformations, add the object, and then - // restore the graphics state - pdfCanvas.saveState(); - pdfCanvas.rectangle(x, y, width, height); - pdfCanvas.clip(); - pdfCanvas.addXObject(formXObject, -x, -y); - pdfCanvas.restoreState(); - } - - outputPdf.close(); - byte[] pdfContent = baos.toByteArray(); - pdfDoc.close(); - return WebResponseUtils.bytesToWebResponse(pdfContent, - file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf"); + +PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(file.getBytes())); + +PDDocument newDocument = new PDDocument(); + +int totalPages = sourceDocument.getNumberOfPages(); + +LayerUtility layerUtility = new LayerUtility(newDocument); + +for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); + + // Create a new page with the size of the source page + PDPage newPage = new PDPage(sourcePage.getMediaBox()); + newDocument.addPage(newPage); + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + + // Import the source page as a form XObject + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + + contentStream.saveGraphicsState(); + + // Define the crop area + contentStream.addRect(x, y, width, height); + contentStream.clip(); + + // Draw the entire formXObject + contentStream.drawForm(formXObject); + + contentStream.restoreGraphicsState(); + + contentStream.close(); + + // Now, set the new page's media box to the cropped size + newPage.setMediaBox(new PDRectangle(x, y, width, height)); +} + +ByteArrayOutputStream baos = new ByteArrayOutputStream(); +newDocument.save(baos); +newDocument.close(); +sourceDocument.close(); + +byte[] pdfContent = baos.toByteArray(); +return WebResponseUtils.bytesToWebResponse(pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf"); } } From 18172aa33a1f9fa402e30e4c66f074136cfbf9db Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:23:44 +0100 Subject: [PATCH 05/14] complete itext removal --- README.md | 1 - build.gradle | 1 - .../SPDF/controller/api/CropController.java | 7 - .../api/converters/ConvertEpubToPdf.java | 2 +- .../api/other/FakeScanControllerWIP.java | 2 +- .../api/security/CertSignController.java | 439 +++++++++--------- .../api/security/CertSignController2.java | 258 ---------- .../software/SPDF/utils/WebResponseUtils.java | 16 +- 8 files changed, 226 insertions(+), 500 deletions(-) delete mode 100644 src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java diff --git a/README.md b/README.md index 91a5e405..50772204 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) h ## Technologies used - Spring Boot + Thymeleaf - PDFBox -- IText7 - [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF) - HTML, CSS, JavaScript diff --git a/build.gradle b/build.gradle index 33d9e069..b628a2d4 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,6 @@ dependencies { implementation 'org.apache.pdfbox:xmpbox:2.0.29' implementation 'org.bouncycastle:bcprov-jdk15on:1.70' implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' - implementation 'com.itextpdf:itext7-core:7.2.5' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-core' implementation group: 'com.google.zxing', name: 'core', version: '3.5.1' diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index 02f40bd1..75152d82 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -15,13 +15,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.geom.PageSize; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.PdfWriter; -import com.itextpdf.kernel.pdf.canvas.PdfCanvas; -import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java index d05e42df..21f4612b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java @@ -71,7 +71,7 @@ public class ConvertEpubToPdf { // Assuming a pseudo-code function that merges multiple PDFs into one. private byte[] mergeMultiplePdfsIntoOne(List individualPdfs) { - // You can use a library such as iText or PDFBox to perform the merging here. + // You can use a library such as PDFBox to perform the merging here. // Return the byte[] of the merged PDF. return null; } diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java index 7a25d28c..37871b73 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java @@ -34,7 +34,7 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.io.source.ByteArrayOutputStream; +import java.io.ByteArrayOutputStream; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; 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 55000dc7..f969f210 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.io.ByteArrayInputStream; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; @@ -18,7 +19,7 @@ import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.List; - +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.util.io.pem.PemReader; import org.slf4j.Logger; @@ -30,267 +31,273 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.io.font.constants.StandardFonts; -import com.itextpdf.kernel.font.PdfFont; -import com.itextpdf.kernel.font.PdfFontFactory; -import com.itextpdf.kernel.geom.Rectangle; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfPage; -import com.itextpdf.kernel.pdf.PdfReader; -import com.itextpdf.kernel.pdf.StampingProperties; -import com.itextpdf.signatures.BouncyCastleDigest; -import com.itextpdf.signatures.DigestAlgorithms; -import com.itextpdf.signatures.IExternalDigest; -import com.itextpdf.signatures.IExternalSignature; -import com.itextpdf.signatures.PdfPKCS7; -import com.itextpdf.signatures.PdfSignatureAppearance; -import com.itextpdf.signatures.PdfSigner; -import com.itextpdf.signatures.PrivateKeySignature; -import com.itextpdf.signatures.SignatureUtil; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSException; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; +import org.bouncycastle.cms.CMSTypedData; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; + +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.Security; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ContentDisposition; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; + +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + @RestController @Tag(name = "Security", description = "Security APIs") public class CertSignController { - private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); + private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); - static { - Security.addProvider(new BouncyCastleProvider()); - } + static { + Security.addProvider(new BouncyCastleProvider()); + } - @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:MF-SISO") - public ResponseEntity signPDF( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file to be signed") - MultipartFile pdf, + @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:MF-SISO") + public ResponseEntity signPDF2( + @RequestPart(required = true, value = "fileInput") @Parameter(description = "The input PDF file to be signed") MultipartFile pdf, - @RequestParam(value = "certType", required = false) - @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) - String certType, + @RequestParam(value = "certType", required = false) @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = { + "PKCS12", "PEM" })) String certType, - @RequestParam(value = "key", required = false) - @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") - MultipartFile privateKeyFile, + @RequestParam(value = "key", required = false) @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") MultipartFile privateKeyFile, - @RequestParam(value = "cert", required = false) - @Parameter(description = "The digital certificate (required for PEM type certificates)") - MultipartFile certFile, + @RequestParam(value = "cert", required = false) @Parameter(description = "The digital certificate (required for PEM type certificates)") MultipartFile certFile, - @RequestParam(value = "p12", required = false) - @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") - MultipartFile p12File, + @RequestParam(value = "p12", required = false) @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") MultipartFile p12File, - @RequestParam(value = "password", required = false) - @Parameter(description = "The password for the keystore or the private key") - String password, + @RequestParam(value = "password", required = false) @Parameter(description = "The password for the keystore or the private key") String password, - @RequestParam(value = "showSignature", required = false) - @Parameter(description = "Whether to visually show the signature in the PDF file") - Boolean showSignature, + @RequestParam(value = "showSignature", required = false) @Parameter(description = "Whether to visually show the signature in the PDF file") Boolean showSignature, - @RequestParam(value = "reason", required = false) - @Parameter(description = "The reason for signing the PDF") - String reason, + @RequestParam(value = "reason", required = false) @Parameter(description = "The reason for signing the PDF") String reason, - @RequestParam(value = "location", required = false) - @Parameter(description = "The location where the PDF is signed") - String location, + @RequestParam(value = "location", required = false) @Parameter(description = "The location where the PDF is signed") String location, - @RequestParam(value = "name", required = false) - @Parameter(description = "The name of the signer") - String name, + @RequestParam(value = "name", required = false) @Parameter(description = "The name of the signer") String name, - @RequestParam(value = "pageNumber", required = false) - @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") - Integer pageNumber) throws Exception { - - BouncyCastleProvider provider = new BouncyCastleProvider(); - Security.addProvider(provider); + @RequestParam(value = "pageNumber", required = false) @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") Integer pageNumber) + throws Exception { - PrivateKey privateKey = null; - X509Certificate cert = null; - - if (certType != null) { - switch (certType) { - case "PKCS12": - if (p12File != null) { - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); - String alias = ks.aliases().nextElement(); - privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); - cert = (X509Certificate) ks.getCertificate(alias); - } - break; - case "PEM": - if (privateKeyFile != null && certFile != null) { - // Load private key - KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider); - if (isPEM(privateKeyFile.getBytes())) { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); - } else { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); - } + PrivateKey privateKey = null; + X509Certificate cert = null; - // Load certificate - CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); - if (isPEM(certFile.getBytes())) { - cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); - } else { - cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); - } - } - break; - } - } + if (certType != null) { + logger.info("Cert type provided: {}", certType); + switch (certType) { + case "PKCS12": + if (p12File != null) { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); + String alias = ks.aliases().nextElement(); + if (!ks.isKeyEntry(alias)) { + throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key."); + } + privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + cert = (X509Certificate) ks.getCertificate(alias); + } + break; + case "PEM": + if (privateKeyFile != null && certFile != null) { + // Load private key + KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(privateKeyFile.getBytes())) { + privateKey = keyFactory + .generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); + } else { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); + } - Principal principal = cert.getSubjectDN(); - String dn = principal.getName(); + // Load certificate + CertificateFactory certFactory = CertificateFactory.getInstance("X.509", + BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(certFile.getBytes())) { + cert = (X509Certificate) certFactory + .generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); + } else { + cert = (X509Certificate) certFactory + .generateCertificate(new ByteArrayInputStream(certFile.getBytes())); + } + } + break; + } + } + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); - // Extract the "CN" (Common Name) field from the distinguished name (if it's present) - String cn = null; - for (String part : dn.split(",")) { - if (part.trim().startsWith("CN=")) { - cn = part.trim().substring("CN=".length()); - break; - } - } - - // Set up the PDF reader and stamper - PdfReader reader = new PdfReader(new ByteArrayInputStream(pdf.getBytes())); - ByteArrayOutputStream signedPdf = new ByteArrayOutputStream(); - PdfSigner signer = new PdfSigner(reader, signedPdf, new StampingProperties()); + // Load the PDF + try (PDDocument document = PDDocument.load(pdf.getBytes())) { + logger.info("Successfully loaded the provided PDF"); + SignatureOptions signatureOptions = new SignatureOptions(); - // Set up the signing appearance - PdfSignatureAppearance appearance = signer.getSignatureAppearance() - .setReason("Test") - .setLocation("TestLocation"); + // If you want to show the signature - if (showSignature != null && showSignature) { - float fontSize = 4; // the font size of the signature - float marginRight = 36; // Margin from the right - float marginBottom = 36; // Margin from the bottom - String signingDate = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date()); + // ATTEMPT 2 + if (showSignature != null && showSignature) { + PDPage page = document.getPage(pageNumber - 1); - // Prepare the text for the digital signature - StringBuilder layer2TextBuilder = new StringBuilder(String.format("Digitally signed by: %s\nDate: %s", - name != null ? name : "Unknown", signingDate)); + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm == null) { + acroForm = new PDAcroForm(document); + document.getDocumentCatalog().setAcroForm(acroForm); + } - if (reason != null && !reason.isEmpty()) { - layer2TextBuilder.append("\nReason: ").append(reason); - } + // Create a new signature field and widget - if (location != null && !location.isEmpty()) { - layer2TextBuilder.append("\nLocation: ").append(location); - } - String layer2Text = layer2TextBuilder.toString(); - // Get the PDF font and measure the width and height of the text block - PdfFont font = PdfFontFactory.createFont(StandardFonts.HELVETICA_BOLD); - float textWidth = Arrays.stream(layer2Text.split("\n")) - .map(line -> font.getWidth(line, fontSize)) - .max(Float::compare) - .orElse(0f); - int numLines = layer2Text.split("\n").length; - float textHeight = numLines * fontSize; + PDSignatureField signatureField = new PDSignatureField(acroForm); + PDAnnotationWidget widget = signatureField.getWidgets().get(0); + PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here + widget.setRectangle(rect); + page.getAnnotations().add(widget); - // Calculate the signature rectangle size - float sigWidth = textWidth + marginRight * 2; - float sigHeight = textHeight + marginBottom * 2; +// Set the appearance for the signature field + PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); + PDAppearanceStream appearanceStream = new PDAppearanceStream(document); + appearanceStream.setResources(new PDResources()); + appearanceStream.setBBox(rect); + appearanceDict.setNormalAppearance(appearanceStream); + widget.setAppearance(appearanceDict); - // Get the page size - PdfPage page = signer.getDocument().getPage(1); - Rectangle pageSize = page.getPageSize(); + try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { + contentStream.beginText(); + contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); + contentStream.newLineAtOffset(110, 130); + contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown")); + contentStream.newLineAtOffset(0, -15); + contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date())); + contentStream.newLineAtOffset(0, -15); + if (reason != null && !reason.isEmpty()) { + contentStream.showText("Reason: " + reason); + contentStream.newLineAtOffset(0, -15); + } + if (location != null && !location.isEmpty()) { + contentStream.showText("Location: " + location); + contentStream.newLineAtOffset(0, -15); + } + contentStream.endText(); + } - // Define the position and dimension of the signature field - Rectangle rect = new Rectangle( - pageSize.getRight() - sigWidth - marginRight, - pageSize.getBottom() + marginBottom, - sigWidth, - sigHeight - ); + // Add the widget annotation to the page + page.getAnnotations().add(widget); - // Configure the appearance of the digital signature - appearance.setPageRect(rect) - .setContact(name != null ? name : "") - .setPageNumber(pageNumber) - .setReason(reason != null ? reason : "") - .setLocation(location != null ? location : "") - .setReuseAppearance(false) - .setLayer2Text(layer2Text.toString()); + // Add the signature field to the acroform + acroForm.getFields().add(signatureField); - signer.setFieldName("sig"); - } else { - appearance.setRenderingMode(PdfSignatureAppearance.RenderingMode.DESCRIPTION); - } - - // Set up the signer - PrivateKeySignature pks = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName()); - IExternalSignature pss = new PrivateKeySignature(privateKey, DigestAlgorithms.SHA256, provider.getName()); - IExternalDigest digest = new BouncyCastleDigest(); + // Handle multiple signatures by ensuring a unique field name + String baseFieldName = "Signature"; + String signatureFieldName = baseFieldName; + int suffix = 1; + while (acroForm.getField(signatureFieldName) != null) { + suffix++; + signatureFieldName = baseFieldName + suffix; + } + signatureField.setPartialName(signatureFieldName); + } + + document.addSignature(signature, signatureOptions); + logger.info("Signature added to the PDF document"); + // External signing + ExternalSigningSupport externalSigning = document + .saveIncrementalForExternalSigning(new ByteArrayOutputStream()); - // Call iTex7 to sign the PDF - signer.signDetached(digest, pks, new Certificate[] {cert}, null, null, null, 0, PdfSigner.CryptoStandard.CMS); + byte[] content = IOUtils.toByteArray(externalSigning.getContent()); - - System.out.println("Signed PDF size: " + signedPdf.size()); + // Using BouncyCastle to sign + CMSTypedData cmsData = new CMSProcessableByteArray(content); - System.out.println("PDF signed = " + isPdfSigned(signedPdf.toByteArray())); - return WebResponseUtils.bytesToWebResponse(signedPdf.toByteArray(), "example.pdf"); - } + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") + .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey); -public boolean isPdfSigned(byte[] pdfData) throws IOException { - InputStream pdfStream = new ByteArrayInputStream(pdfData); - PdfDocument pdfDoc = new PdfDocument(new PdfReader(pdfStream)); - SignatureUtil signatureUtil = new SignatureUtil(pdfDoc); - List names = signatureUtil.getSignatureNames(); + gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) + .build(signer, cert)); - boolean isSigned = false; + gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); + CMSSignedData signedData = gen.generate(cmsData, false); - for (String name : names) { - PdfPKCS7 pkcs7 = signatureUtil.readSignatureData(name); - if (pkcs7 != null) { - System.out.println("Signature found."); + byte[] cmsSignature = signedData.getEncoded(); + logger.info("About to sign content using BouncyCastle"); + externalSigning.setSignature(cmsSignature); + logger.info("Signature set successfully"); - // Log certificate details - Certificate[] signChain = pkcs7.getSignCertificateChain(); - for (Certificate cert : signChain) { - if (cert instanceof X509Certificate) { - X509Certificate x509 = (X509Certificate) cert; - System.out.println("Certificate Details:"); - System.out.println("Subject: " + x509.getSubjectDN()); - System.out.println("Issuer: " + x509.getIssuerDN()); - System.out.println("Serial: " + x509.getSerialNumber()); - System.out.println("Not Before: " + x509.getNotBefore()); - System.out.println("Not After: " + x509.getNotAfter()); - } - } + // After setting the signature, return the resultant PDF + try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { + document.save(signedPdfOutput); + return WebResponseUtils.boasToWebResponse(signedPdfOutput, + pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); - isSigned = true; - } - } + } catch (Exception e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } - pdfDoc.close(); + return null; + } - return isSigned; -} - private byte[] parsePEM(byte[] content) throws IOException { - PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); - return pemReader.readPemObject().getContent(); - } - - private boolean isPEM(byte[] content) { - String contentStr = new String(content); - return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); - } - - - + private byte[] parsePEM(byte[] content) throws IOException { + PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + return pemReader.readPemObject().getContent(); + } + private boolean isPEM(byte[] content) { + String contentStr = new String(content); + return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java deleted file mode 100644 index e159b2a3..00000000 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController2.java +++ /dev/null @@ -1,258 +0,0 @@ -package stirling.software.SPDF.controller.api.security; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.KeyFactory; -import java.security.KeyStore; -import java.security.Principal; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.Certificate; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.spec.PKCS8EncodedKeySpec; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.List; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.util.io.pem.PemReader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -import org.bouncycastle.cert.jcajce.JcaCertStore; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.CMSSignedDataGenerator; -import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.cms.CMSTypedData; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Collections; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ContentDisposition; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.http.ResponseEntity; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.tags.Tag; -import stirling.software.SPDF.utils.WebResponseUtils; - -import org.apache.commons.io.IOUtils; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -@RestController -@Tag(name = "Security", description = "Security APIs") -public class CertSignController2 { - - private static final Logger logger = LoggerFactory.getLogger(CertSignController2.class); - - static { - Security.addProvider(new BouncyCastleProvider()); - } - - @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:MF-SISO") - public ResponseEntity signPDF( - @RequestPart(required = true, value = "fileInput") - @Parameter(description = "The input PDF file to be signed") - MultipartFile pdf, - - @RequestParam(value = "certType", required = false) - @Parameter(description = "The type of the digital certificate", schema = @Schema(allowableValues = {"PKCS12", "PEM"})) - String certType, - - @RequestParam(value = "key", required = false) - @Parameter(description = "The private key for the digital certificate (required for PEM type certificates)") - MultipartFile privateKeyFile, - - @RequestParam(value = "cert", required = false) - @Parameter(description = "The digital certificate (required for PEM type certificates)") - MultipartFile certFile, - - @RequestParam(value = "p12", required = false) - @Parameter(description = "The PKCS12 keystore file (required for PKCS12 type certificates)") - MultipartFile p12File, - - @RequestParam(value = "password", required = false) - @Parameter(description = "The password for the keystore or the private key") - String password, - - @RequestParam(value = "showSignature", required = false) - @Parameter(description = "Whether to visually show the signature in the PDF file") - Boolean showSignature, - - @RequestParam(value = "reason", required = false) - @Parameter(description = "The reason for signing the PDF") - String reason, - - @RequestParam(value = "location", required = false) - @Parameter(description = "The location where the PDF is signed") - String location, - - @RequestParam(value = "name", required = false) - @Parameter(description = "The name of the signer") - String name, - - @RequestParam(value = "pageNumber", required = false) - @Parameter(description = "The page number where the signature should be visible. This is required if showSignature is set to true") - Integer pageNumber) throws Exception { - - BouncyCastleProvider provider = new BouncyCastleProvider(); - Security.addProvider(provider); - - PrivateKey privateKey = null; - X509Certificate cert = null; - - if (certType != null) { - switch (certType) { - case "PKCS12": - if (p12File != null) { - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); - String alias = ks.aliases().nextElement(); - privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); - cert = (X509Certificate) ks.getCertificate(alias); - } - break; - case "PEM": - if (privateKeyFile != null && certFile != null) { - // Load private key - KeyFactory keyFactory = KeyFactory.getInstance("RSA", provider); - if (isPEM(privateKeyFile.getBytes())) { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); - } else { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); - } - - // Load certificate - CertificateFactory certFactory = CertificateFactory.getInstance("X.509", provider); - if (isPEM(certFile.getBytes())) { - cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); - } else { - cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certFile.getBytes())); - } - } - break; - } - } - PDSignature signature = new PDSignature(); - signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter - signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED); - signature.setName(name); - signature.setLocation(location); - signature.setReason(reason); - - // Load the PDF - try (PDDocument document = PDDocument.load(pdf.getBytes())) { - SignatureOptions signatureOptions = new SignatureOptions(); - - // If you want to show the signature - if (showSignature != null && showSignature) { - // Calculate signature field position based on your requirements - - PDPage page = document.getPage(pageNumber - 1); // zero-based - - PDVisibleSignDesigner signDesigner = new PDVisibleSignDesigner(new ByteArrayInputStream(pdf.getBytes())); - //TODO signDesigner - - PDVisibleSigProperties sigProperties = new PDVisibleSigProperties(); - - //TODO sigProperties extra - signatureOptions.setVisualSignature(sigProperties); - signatureOptions.setPage(pageNumber - 1); - } - - document.addSignature(signature, signatureOptions); - - // External signing - ExternalSigningSupport externalSigning = document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); - - byte[] content = IOUtils.toByteArray(externalSigning.getContent()); - - // Using BouncyCastle to sign - CMSTypedData cmsData = new CMSProcessableByteArray(content); - - CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider).build(privateKey); - - gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( - new JcaDigestCalculatorProviderBuilder().setProvider(provider).build()).build(signer, cert)); - - gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); - CMSSignedData signedData = gen.generate(cmsData, false); - - byte[] cmsSignature = signedData.getEncoded(); - - externalSigning.setSignature(cmsSignature); - - - // After setting the signature, return the resultant PDF - try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { - document.save(signedPdfOutput); - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_PDF); - headers.setContentDisposition(ContentDisposition.builder("attachment").filename("signed.pdf").build()); - - return new ResponseEntity<>(signedPdfOutput.toByteArray(), headers, HttpStatus.OK); - } - } - - - } - - private byte[] parsePEM(byte[] content) throws IOException { - PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); - return pemReader.readPemObject().getContent(); - } - - private boolean isPEM(byte[] content) { - String contentStr = new String(content); - return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); - } - - - - - -} diff --git a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java index 09a395ba..131aaf03 100644 --- a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java @@ -12,8 +12,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; -import com.itextpdf.kernel.pdf.PdfDocument; -import com.itextpdf.kernel.pdf.PdfWriter; public class WebResponseUtils { @@ -61,18 +59,6 @@ public class WebResponseUtils { return boasToWebResponse(baos, docName); } - public static ResponseEntity pdfDocToWebResponse(PdfDocument document, String docName) throws IOException { - - // Open Byte Array and save document to it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - PdfWriter writer = new PdfWriter(baos); - PdfDocument newDocument = new PdfDocument(writer); - - document.copyPagesTo(1, document.getNumberOfPages(), newDocument); - newDocument.close(); - - return boasToWebResponse(baos, docName); - } - + } From 9fe96bec40558164f9c985dac6f2f3629ae0ea3b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 3 Sep 2023 01:24:02 +0100 Subject: [PATCH 06/14] import cleanup --- .../software/SPDF/SPdfApplication.java | 1 - .../software/SPDF/config/OpenApiConfig.java | 4 - .../config/security/InitialSecuritySetup.java | 2 - .../security/SecurityConfiguration.java | 5 +- .../SPDF/controller/api/CropController.java | 24 +----- .../api/MultiPageLayoutController.java | 5 +- .../controller/api/ScalePagesController.java | 22 +---- .../api/ToSinglePageController.java | 9 +- .../SPDF/controller/api/UserController.java | 1 - .../api/other/FakeScanControllerWIP.java | 3 +- .../api/other/PageNumbersController.java | 4 - .../api/security/CertSignController.java | 85 ++++++------------- .../controller/api/security/GetInfoOnPDF.java | 16 ++-- .../controller/web/AccountWebController.java | 16 ---- 14 files changed, 46 insertions(+), 151 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 65542e17..5dd7fed3 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -9,7 +9,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; - import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.utils.GeneralUtils; diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index d3cade1a..2583277e 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,9 +1,5 @@ package stirling.software.SPDF.config; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 7bb13535..159b8b03 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -11,10 +11,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; -import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; -import stirling.software.SPDF.model.*; @Component public class InitialSecuritySetup { diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index 2e0b8345..e0afcb9a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -1,15 +1,14 @@ package stirling.software.SPDF.config.security; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; - import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index 75152d82..9e8cf1fe 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -4,9 +4,12 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -15,32 +18,11 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; -import org.apache.commons.io.IOUtils; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.font.PDType0Font; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; -import org.apache.pdfbox.util.Matrix; -import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; -import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; -import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentGroup; -import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties; -import stirling.software.SPDF.utils.WebResponseUtils; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index 13a5e47f..738542a0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -4,11 +4,13 @@ package stirling.software.SPDF.controller.api; import java.io.ByteArrayOutputStream; import java.io.IOException; +import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.util.Matrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; @@ -16,10 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.apache.pdfbox.util.Matrix; - -import org.apache.pdfbox.multipdf.LayerUtility; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index 26ff323f..2f1cde7d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -1,44 +1,30 @@ package stirling.software.SPDF.controller.api; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Set; -import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; -import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.util.Matrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.graphics.PDXObject; -import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; - -import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class ScalePagesController { diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 1c35a93f..e46cb55a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -1,15 +1,14 @@ package stirling.software.SPDF.controller.api; import java.awt.geom.AffineTransform; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; +import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,14 +18,10 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; - - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.multipdf.LayerUtility; @RestController @Tag(name = "General", description = "General APIs") public class ToSinglePageController { diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index dbb18df5..9eaa05bd 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -13,7 +13,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -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.RequestParam; diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java index 37871b73..19c5aa5e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/FakeScanControllerWIP.java @@ -9,6 +9,7 @@ import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.RescaleOp; +import java.io.ByteArrayOutputStream; //Required for file input/output import java.io.File; import java.io.IOException; @@ -34,8 +35,6 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.ByteArrayOutputStream; - import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java index d1a469d0..8e38bd10 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/other/PageNumbersController.java @@ -1,9 +1,7 @@ package stirling.software.SPDF.controller.api.other; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.URLEncoder; import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; @@ -20,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; - - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; 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 f969f210..aaaf22b4 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,26 +1,45 @@ package stirling.software.SPDF.controller.api.security; import java.io.ByteArrayInputStream; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceCharacteristicsDictionary; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.InputStreamReader; import java.security.KeyFactory; import java.security.KeyStore; -import java.security.Principal; import java.security.PrivateKey; import java.security.Security; -import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.spec.PKCS8EncodedKeySpec; import java.text.SimpleDateFormat; -import java.util.Arrays; +import java.util.Collections; import java.util.Date; -import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; +import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; +import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; +import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; +import org.bouncycastle.cert.jcajce.JcaCertStore; +import org.bouncycastle.cms.CMSProcessableByteArray; +import org.bouncycastle.cms.CMSSignedData; +import org.bouncycastle.cms.CMSSignedDataGenerator; +import org.bouncycastle.cms.CMSTypedData; +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; import org.bouncycastle.util.io.pem.PemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,66 +50,12 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.bouncycastle.cert.jcajce.JcaCertStore; -import org.bouncycastle.cms.CMSException; -import org.bouncycastle.cms.CMSProcessableByteArray; -import org.bouncycastle.cms.CMSSignedData; -import org.bouncycastle.cms.CMSSignedDataGenerator; -import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder; -import org.bouncycastle.cms.CMSTypedData; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; -import org.apache.pdfbox.pdmodel.PDPageContentStream; -import org.apache.pdfbox.pdmodel.PDResources; - -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.Security; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Collections; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.ContentDisposition; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.http.ResponseEntity; - import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; import stirling.software.SPDF.utils.WebResponseUtils; -import org.apache.commons.io.IOUtils; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.ExternalSigningSupport; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureOptions; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSignDesigner; -import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; -import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; -import org.apache.pdfbox.pdmodel.interactive.digitalsignature.visible.PDVisibleSigProperties; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - @RestController @Tag(name = "Security", description = "Security APIs") public class CertSignController { diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index e75fe502..1f0e7d62 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -1,7 +1,7 @@ package stirling.software.SPDF.controller.api.security; +import java.io.ByteArrayOutputStream; import java.io.IOException; -import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Calendar; @@ -10,10 +10,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.io.ByteArrayOutputStream; -import org.apache.xmpbox.xml.DomXmpParser; -import org.apache.commons.io.IOUtils; -import org.apache.pdfbox.cos.COSDictionary; + import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSInputStream; import org.apache.pdfbox.cos.COSName; @@ -28,7 +25,6 @@ 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.COSObjectable; import org.apache.pdfbox.pdmodel.common.PDMetadata; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDStream; @@ -37,6 +33,7 @@ import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureElement; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureNode; import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot; +import org.apache.pdfbox.pdmodel.encryption.AccessPermission; import org.apache.pdfbox.pdmodel.encryption.PDEncryption; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDFontDescriptor; @@ -57,15 +54,16 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.xmpbox.XMPMetadata; +import org.apache.xmpbox.xml.DomXmpParser; +import org.apache.xmpbox.xml.XmpParsingException; +import org.apache.xmpbox.xml.XmpSerializer; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import org.apache.xmpbox.XMPMetadata; -import org.apache.xmpbox.xml.XmpParsingException; -import org.apache.xmpbox.xml.XmpSerializer; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index c4c747aa..b86e6e86 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -1,22 +1,8 @@ package stirling.software.SPDF.controller.web; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.UserDetails; @@ -27,10 +13,8 @@ import org.springframework.web.bind.annotation.GetMapping; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; -import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.UserRepository; @Controller From 146dd3c00b1b5c76410d165e5d20b9b1a04a50ae Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:40:40 +0100 Subject: [PATCH 07/14] cred change start --- .../software/SPDF/config/AppConfig.java | 1 - .../config/security/FirstLoginFilter.java | 53 ++++++++ .../config/security/InitialSecuritySetup.java | 2 +- .../security/SecurityConfiguration.java | 4 + .../SPDF/config/security/UserService.java | 17 +++ .../SPDF/controller/api/UserController.java | 116 ++++++++++++++---- .../controller/web/AccountWebController.java | 31 +++++ .../stirling/software/SPDF/model/User.java | 12 +- src/main/resources/messages_en_GB.properties | 2 +- src/main/resources/templates/account.html | 7 +- .../resources/templates/change-creds.html | 60 +++++++++ 11 files changed, 275 insertions(+), 30 deletions(-) create mode 100644 src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java create mode 100644 src/main/resources/templates/change-creds.html diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index c2b26538..309437b6 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -13,7 +13,6 @@ public class AppConfig { @Bean(name = "loginEnabled") public boolean loginEnabled() { - System.out.println(applicationProperties.toString()); return applicationProperties.getSecurity().getEnableLogin(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java new file mode 100644 index 00000000..8e612464 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -0,0 +1,53 @@ +package stirling.software.SPDF.config.security; + +import java.io.IOException; +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; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import stirling.software.SPDF.model.User; + +@Component +public class FirstLoginFilter extends OncePerRequestFilter { + + @Autowired + @Lazy + private UserService userService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + // Check if the request is for static resources + boolean isStaticResource = requestURI.startsWith("/css/") + || requestURI.startsWith("/js/") + || requestURI.startsWith("/images/") + || requestURI.startsWith("/public/") + || requestURI.endsWith(".svg"); + + // 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 = userService.findByUsername(authentication.getName()); + if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { + response.sendRedirect("/change-creds"); + return; + } + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 159b8b03..7097020b 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -28,7 +28,7 @@ public class InitialSecuritySetup { if (!userService.hasUsers()) { String initialUsername = "admin"; String initialPassword = "stirling"; - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index e0afcb9a..dfe782ac 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -41,6 +41,9 @@ public class SecurityConfiguration { @Autowired private UserAuthenticationFilter userAuthenticationFilter; + @Autowired + private FirstLoginFilter firstLoginFilter; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); @@ -48,6 +51,7 @@ public class SecurityConfiguration { if(loginEnabledValue) { http.csrf(csrf -> csrf.disable()); + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); http .formLogin(formLogin -> formLogin .loginPage("/login") diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 19bf007b..46c5aeff 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -113,12 +113,23 @@ public class UserService { userRepository.save(user); } + public void saveUser(String username, String password, String role, boolean firstLogin) { + User user = new User(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(password)); + user.addAuthority(new Authority(role, user)); + user.setEnabled(true); + user.setFirstLogin(firstLogin); + userRepository.save(user); + } + public void saveUser(String username, String password, String role) { User user = new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(password)); user.addAuthority(new Authority(role, user)); user.setEnabled(true); + user.setFirstLogin(false); userRepository.save(user); } @@ -168,6 +179,12 @@ public class UserService { userRepository.save(user); } + public void changeFirstUse(User user, boolean firstUse) { + user.setFirstLogin(firstUse); + userRepository.save(user); + } + + public boolean isPasswordCorrect(User user, String currentPassword) { return passwordEncoder.matches(currentPassword, user.getPassword()); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index 9eaa05bd..f232daf1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -16,6 +16,8 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -39,25 +41,80 @@ public class UserController { return "redirect:/login?registered=true"; } - @PostMapping("/change-username") - public ResponseEntity changeUsername(Principal principal, @RequestParam String currentPassword, @RequestParam String newUsername, HttpServletRequest request, HttpServletResponse response) { + @PostMapping("/change-username-and-password") + public RedirectView changeUsernameAndPassword(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { if (principal == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/error"); } - + Optional userOpt = userService.findByUsername(principal.getName()); - - if(userOpt == null || userOpt.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/error"); } User user = userOpt.get(); - - if(!userService.isPasswordCorrect(user, currentPassword)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/error"); } - - if(userService.usernameExists(newUsername)) { - return ResponseEntity.status(HttpStatus.CONFLICT).body("New username already exists."); + + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + redirectAttributes.addFlashAttribute("error", "New username already exists."); + return new RedirectView("/error"); + } + + userService.changePassword(user, newPassword); + if(!user.getUsername().equals(newUsername)) { + userService.changeUsername(user, newUsername); + } + userService.changeFirstUse(user, false); + + // Logout using Spring's utility + new SecurityContextLogoutHandler().logout(request, response, null); + + redirectAttributes.addFlashAttribute("credsUpdated", true); + return new RedirectView("/login"); + } + + + + @PostMapping("/change-username") + public RedirectView changeUsername(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/account"); + } + + Optional userOpt = userService.findByUsername(principal.getName()); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/account"); + } + User user = userOpt.get(); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/account"); + } + + if (userService.usernameExists(newUsername)) { + redirectAttributes.addFlashAttribute("error", "New username already exists."); + return new RedirectView("/account"); } userService.changeUsername(user, newUsername); @@ -65,33 +122,44 @@ public class UserController { // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - - return ResponseEntity.ok("Username updated successfully."); + redirectAttributes.addFlashAttribute("message", "Username updated successfully."); + return new RedirectView("/login"); } @PostMapping("/change-password") - public ResponseEntity changePassword(Principal principal, @RequestParam String currentPassword, @RequestParam String newPassword, HttpServletRequest request, HttpServletResponse response) { + public RedirectView changePassword(Principal principal, + @RequestParam String currentPassword, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { if (principal == null) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated."); + redirectAttributes.addFlashAttribute("error", "User not authenticated."); + return new RedirectView("/account"); } Optional userOpt = userService.findByUsername(principal.getName()); - - if(userOpt == null || userOpt.isEmpty()) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("User not found."); + + if (userOpt == null || userOpt.isEmpty()) { + redirectAttributes.addFlashAttribute("error", "User not found."); + return new RedirectView("/account"); } User user = userOpt.get(); - if(!userService.isPasswordCorrect(user, currentPassword)) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Current password is incorrect."); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + return new RedirectView("/account"); } userService.changePassword(user, newPassword); // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - - return ResponseEntity.ok("Password updated successfully."); + + redirectAttributes.addFlashAttribute("message", "Password updated successfully."); + return new RedirectView("/login"); } + @PostMapping("/updateUserSettings") public String updateUserSettings(HttpServletRequest request, Principal principal) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index b86e6e86..cba7ebb5 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -91,6 +91,7 @@ public class AccountWebController { model.addAttribute("username", username); model.addAttribute("role", user.get().getRolesAsString()); model.addAttribute("settings", settingsJson); + model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); } } else { return "redirect:/"; @@ -100,5 +101,35 @@ public class AccountWebController { + @GetMapping("/change-creds") + public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/"; + } + 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 = userRepository.findByUsername(username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } + // Add attributes to the model + model.addAttribute("username", username); + } + } else { + return "redirect:/"; + } + return "change-creds"; + } + } diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index 2881e308..f771a821 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -40,6 +40,9 @@ public class User { @Column(name = "enabled") private boolean enabled; + @Column(name = "isFirstLogin") + private Boolean isFirstLogin = false; + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") private Set authorities = new HashSet<>(); @@ -50,7 +53,14 @@ public class User { private Map settings = new HashMap<>(); // Key-value pairs of settings. - + public boolean isFirstLogin() { + return isFirstLogin != null && isFirstLogin; + } + + public void setFirstLogin(boolean isFirstLogin) { + this.isFirstLogin = isFirstLogin; + } + public Long getId() { return id; } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 03362eec..a5620aa1 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -42,7 +42,7 @@ red=Red green=Green blue=Blue custom=Custom... - +changeCredsMessage=First time login, Please change your username and/or password! diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 5a4882e5..ba104b86 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -16,11 +16,14 @@

User Settings


- +
+

User!

- +

diff --git a/src/main/resources/templates/change-creds.html b/src/main/resources/templates/change-creds.html new file mode 100644 index 00000000..c6d94c66 --- /dev/null +++ b/src/main/resources/templates/change-creds.html @@ -0,0 +1,60 @@ + + + + + + + +
+
+
+

+
+
+
+ + +

User Settings

+
+
+ + +

User!

+ + + +

+

Change Username and password

+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + +
+ + + + + +
+
+ +
+
+
+ + From 0bb2df135b122cca42126bc702b50b4e8c18bdbf Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 3 Sep 2023 19:44:16 +0100 Subject: [PATCH 08/14] testing messages --- .../security/UserAuthenticationFilter.java | 11 +++--- .../SPDF/controller/api/UserController.java | 30 ++++++++-------- src/main/resources/messages_en_GB.properties | 6 +++- src/main/resources/templates/account.html | 16 ++++++++- .../resources/templates/change-creds.html | 12 +++++++ src/main/resources/templates/login.html | 35 +++++++++++-------- 6 files changed, 74 insertions(+), 36 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index 1d5aab88..eca7f70e 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -44,7 +44,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); return; } - + String requestURI = request.getRequestURI(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check for API key in the request headers if no authentication exists @@ -74,13 +74,14 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // If we still don't have any authentication, deny the request if (authentication == null || !authentication.isAuthenticated()) { String method = request.getMethod(); - if ("GET".equalsIgnoreCase(method)) { + if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) { response.sendRedirect("/login"); // redirect to the login page return; + } else { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); + return; } - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); - return; } filterChain.doFilter(request, response); diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index f232daf1..647dca6b 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -50,26 +50,26 @@ public class UserController { HttpServletResponse response, RedirectAttributes redirectAttributes) { if (principal == null) { - redirectAttributes.addFlashAttribute("error", "User not authenticated."); - return new RedirectView("/error"); + redirectAttributes.addFlashAttribute("notAuthenticated", true); + return new RedirectView("/change-creds"); } Optional userOpt = userService.findByUsername(principal.getName()); if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("error", "User not found."); - return new RedirectView("/error"); + redirectAttributes.addFlashAttribute("userNotFound", true); + return new RedirectView("/change-creds"); } User user = userOpt.get(); if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); - return new RedirectView("/error"); + redirectAttributes.addFlashAttribute("incorrectPassword", true); + return new RedirectView("/change-creds"); } if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - redirectAttributes.addFlashAttribute("error", "New username already exists."); - return new RedirectView("/error"); + redirectAttributes.addFlashAttribute("usernameExists", true); + return new RedirectView("/change-creds"); } userService.changePassword(user, newPassword); @@ -95,25 +95,25 @@ public class UserController { HttpServletResponse response, RedirectAttributes redirectAttributes) { if (principal == null) { - redirectAttributes.addFlashAttribute("error", "User not authenticated."); + redirectAttributes.addFlashAttribute("notAuthenticated", true); return new RedirectView("/account"); } Optional userOpt = userService.findByUsername(principal.getName()); if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("error", "User not found."); + redirectAttributes.addFlashAttribute("userNotFound", true); return new RedirectView("/account"); } User user = userOpt.get(); if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + redirectAttributes.addFlashAttribute("incorrectPassword", true); return new RedirectView("/account"); } if (userService.usernameExists(newUsername)) { - redirectAttributes.addFlashAttribute("error", "New username already exists."); + redirectAttributes.addFlashAttribute("usernameExists", true); return new RedirectView("/account"); } @@ -134,20 +134,20 @@ public class UserController { HttpServletResponse response, RedirectAttributes redirectAttributes) { if (principal == null) { - redirectAttributes.addFlashAttribute("error", "User not authenticated."); + redirectAttributes.addFlashAttribute("notAuthenticated", true); return new RedirectView("/account"); } Optional userOpt = userService.findByUsername(principal.getName()); if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("error", "User not found."); + redirectAttributes.addFlashAttribute("userNotFound", true); return new RedirectView("/account"); } User user = userOpt.get(); if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("error", "Current password is incorrect."); + redirectAttributes.addFlashAttribute("incorrectPassword", true); return new RedirectView("/account"); } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index a5620aa1..88284dec 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -42,8 +42,12 @@ red=Red green=Green blue=Blue custom=Custom... -changeCredsMessage=First time login, Please change your username and/or password! +changedCredsMessage=Credentials changed! +notAuthenticatedMessage=User not authenticated. +userNotFoundMessage=User not found. +incorrectPasswordMessage=Current password is incorrect. +usernameExistsMessage=New Username already exists. diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index ba104b86..ac855d1b 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -16,7 +16,21 @@

User Settings


-
+ + + + + + +

User!

diff --git a/src/main/resources/templates/change-creds.html b/src/main/resources/templates/change-creds.html index c6d94c66..8f64eb49 100644 --- a/src/main/resources/templates/change-creds.html +++ b/src/main/resources/templates/change-creds.html @@ -16,6 +16,18 @@

User Settings


+ + + +
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 1ddea6ec..a82205d8 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -179,11 +179,13 @@ document.addEventListener('DOMContentLoaded', function() { const urlParams = currentURL.searchParams; const currentLangParam = urlParams.get('lang') || defaultLocale; - console.log("defaultLocale", defaultLocale) - console.log("storedLocale", storedLocale) - console.log("currentLangParam", currentLangParam) + console.log("defaultLocale", defaultLocale); + console.log("storedLocale", storedLocale); + console.log("currentLangParam", currentLangParam); - if (currentLangParam !== storedLocale) { + if (defaultLocale !== storedLocale && currentLangParam !== storedLocale) { + console.log("currentLangParam", currentLangParam) + console.log("storedLocale", storedLocale) urlParams.set('lang', storedLocale); currentURL.search = urlParams.toString(); @@ -235,17 +237,20 @@ function handleDropdownItemClick(event) { event.preventDefault(); const languageCode = event.currentTarget.dataset.bsLanguageCode; const dropdown = document.getElementById('languageDropdown'); - + if (languageCode) { - localStorage.setItem('languageCode', languageCode); - - const currentUrl = window.location.href; - if (currentUrl.indexOf('?lang=') === -1) { - window.location.href = currentUrl + '?lang=' + languageCode; - } else { - window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); - } - + localStorage.setItem('languageCode', languageCode); + const currentLang = document.documentElement.getAttribute('lang'); + if (currentLang !== languageCode) { + console.log("currentLang", currentLang) + console.log("languageCode", languageCode) + const currentUrl = window.location.href; + if (currentUrl.indexOf('?lang=') === -1) { + window.location.href = currentUrl + '?lang=' + languageCode; + } else { + window.location.href = currentUrl.replace(/\?lang=\w{2,}/, '?lang=' + languageCode); + } + } dropdown.innerHTML = event.currentTarget.innerHTML; // Update the dropdown button's content } else { console.error("Language code is not set for this item."); @@ -258,6 +263,8 @@ function handleDropdownItemClick(event) {
+ +

Stirling-PDF

From fd08513212165793041490ccce40aee0f350c05a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 4 Sep 2023 00:12:27 +0100 Subject: [PATCH 09/14] dipslay stuf --- .../SPDF/config/CleanUrlInterceptor.java | 3 +- .../SPDF/config/ConfigInitializer.java | 1 - .../SPDF/controller/api/UserController.java | 103 ++++++++---------- src/main/resources/messages_en_GB.properties | 15 ++- src/main/resources/templates/account.html | 18 +-- .../resources/templates/change-creds.html | 20 ++-- src/main/resources/templates/login.html | 12 +- 7 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index dec37567..4df14253 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -13,7 +13,7 @@ import jakarta.servlet.http.HttpServletResponse; public class CleanUrlInterceptor implements HandlerInterceptor { - private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file"); + private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); @Override @@ -32,7 +32,6 @@ public class CleanUrlInterceptor implements HandlerInterceptor { if (keyValue.length != 2) { continue; } - if (ALLOWED_PARAMS.contains(keyValue[0])) { parameters.put(keyValue[0], keyValue[1]); } diff --git a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java index 49f0c2f7..43d0ccfe 100644 --- a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java +++ b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java @@ -39,5 +39,4 @@ public class ConfigInitializer implements ApplicationContextInitializer userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("userNotFound", true); - return new RedirectView("/change-creds"); - } - User user = userOpt.get(); + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/change-creds?messageType=userNotFound"); + } - if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("incorrectPassword", true); - return new RedirectView("/change-creds"); - } + User user = userOpt.get(); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/change-creds?messageType=incorrectPassword"); + } + + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/change-creds?messageType=usernameExists"); + } - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - redirectAttributes.addFlashAttribute("usernameExists", true); - return new RedirectView("/change-creds"); - } userService.changePassword(user, newPassword); if(!user.getUsername().equals(newUsername)) { @@ -81,8 +79,7 @@ public class UserController { // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - redirectAttributes.addFlashAttribute("credsUpdated", true); - return new RedirectView("/login"); + return new RedirectView("/login?messageType=credsUpdated"); } @@ -94,36 +91,33 @@ public class UserController { HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) { - if (principal == null) { - redirectAttributes.addFlashAttribute("notAuthenticated", true); - return new RedirectView("/account"); - } + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("userNotFound", true); - return new RedirectView("/account"); - } - User user = userOpt.get(); + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("incorrectPassword", true); - return new RedirectView("/account"); - } + User user = userOpt.get(); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } + + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/account?messageType=usernameExists"); + } - if (userService.usernameExists(newUsername)) { - redirectAttributes.addFlashAttribute("usernameExists", true); - return new RedirectView("/account"); - } userService.changeUsername(user, newUsername); // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - redirectAttributes.addFlashAttribute("message", "Username updated successfully."); - return new RedirectView("/login"); + return new RedirectView("/login?messageType=credsUpdated"); } @PostMapping("/change-password") @@ -133,31 +127,28 @@ public class UserController { HttpServletRequest request, HttpServletResponse response, RedirectAttributes redirectAttributes) { - if (principal == null) { - redirectAttributes.addFlashAttribute("notAuthenticated", true); - return new RedirectView("/account"); - } + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - redirectAttributes.addFlashAttribute("userNotFound", true); - return new RedirectView("/account"); - } - User user = userOpt.get(); + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - if (!userService.isPasswordCorrect(user, currentPassword)) { - redirectAttributes.addFlashAttribute("incorrectPassword", true); - return new RedirectView("/account"); - } + User user = userOpt.get(); + + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } userService.changePassword(user, newPassword); // Logout using Spring's utility new SecurityContextLogoutHandler().logout(request, response, null); - redirectAttributes.addFlashAttribute("message", "Password updated successfully."); - return new RedirectView("/login"); + return new RedirectView("/login?messageType=credsUpdated"); } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 88284dec..06681b6f 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -42,8 +42,8 @@ red=Red green=Green blue=Blue custom=Custom... -changedCredsMessage=Credentials changed! +changedCredsMessage=Credentials changed! notAuthenticatedMessage=User not authenticated. userNotFoundMessage=User not found. incorrectPasswordMessage=Current password is incorrect. @@ -75,6 +75,19 @@ settings.zipThreshold=Zip files when the number of downloaded files exceeds settings.signOut=Sign Out settings.accountSettings=Account Settings + + +changeCreds.title=Change Credentials +changeCreds.header=Update Your Account Details +changeCreds.changeUserAndPassword=You are using default login credentials. Please enter a new password (and username if wanted) +changeCreds.newUsername=New Username +changeCreds.oldPassword=Current Password +changeCreds.newPassword=New Password +changeCreds.confirmNewPassword=Confirm New Password +changeCreds.submit=Submit Changes + + + account.title=Account Settings account.accountSettings=Account Settings account.adminSettings=Admin Settings - View and Add Users diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index ac855d1b..4a7a3174 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -16,19 +16,21 @@

User Settings


-