Update AGENTS guidelines (#3556)

## Summary
- clarify Codex contribution instructions
- remove `test.sh` reference and require `./gradlew build`
- add Developer Guide, AI note and translation policy

## Testing
- `./gradlew spotlessApply`
- `./gradlew build`
This commit is contained in:
Anthony Stirling 2025-05-20 12:02:10 +01:00 committed by GitHub
parent 9fe49c494d
commit 218d21f07a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 229 additions and 178 deletions

24
AGENTS.md Normal file
View File

@ -0,0 +1,24 @@
# Codex Contribution Guidelines for Stirling-PDF
This file provides high-level instructions for Codex when modifying any files within this repository. Follow these rules to ensure changes remain consistent with the existing project structure.
## 1. Code Style and Formatting
- Respect the `.editorconfig` settings located in the repository root. Java files use 4 spaces; HTML, JS, and Python generally use 2 spaces. Lines should end with `LF`.
- Format Java code with `./gradlew spotlessApply` before committing.
- Review `DeveloperGuide.md` for project structure and design details before making significant changes.
## 2. Testing
- Run `./gradlew build` before committing changes to ensure the project compiles.
- If the build cannot complete due to environment restrictions, DO NOT COMMIT THE CHANGE
## 3. Commits
- Keep commits focused. Group related changes together and provide concise commit messages.
- Ensure the working tree is clean (`git status`) before concluding your work.
## 4. Pull Requests
- Summarize what was changed and why. Include build results from `./gradlew build` in the PR description.
- Note that the code was generated with the assistance of AI.
## 5. Translations
- Only modify `messages_en_GB.properties` when adding or updating translations.

View File

@ -49,7 +49,8 @@ public class KeygenLicenseVerifier {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
// Shared HTTP client for connection pooling // Shared HTTP client for connection pooling
private static final HttpClient httpClient = HttpClient.newBuilder() private static final HttpClient httpClient =
HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2) .version(HttpClient.Version.HTTP_2)
.connectTimeout(java.time.Duration.ofSeconds(10)) .connectTimeout(java.time.Duration.ofSeconds(10))
.build(); .build();
@ -416,7 +417,9 @@ public class KeygenLicenseVerifier {
if (policyFloating) { if (policyFloating) {
context.isFloatingLicense = true; context.isFloatingLicense = true;
context.maxMachines = policyMaxMachines; context.maxMachines = policyMaxMachines;
log.info("Policy defines floating license with max machines: {}", context.maxMachines); log.info(
"Policy defines floating license with max machines: {}",
context.maxMachines);
} }
// Extract max users and isEnterprise from policy or metadata // Extract max users and isEnterprise from policy or metadata
@ -474,7 +477,8 @@ public class KeygenLicenseVerifier {
activateMachine(licenseKey, licenseId, machineFingerprint, context); activateMachine(licenseKey, licenseId, machineFingerprint, context);
if (activated) { if (activated) {
// Revalidate after activation // Revalidate after activation
validationResponse = validateLicense(licenseKey, machineFingerprint, context); validationResponse =
validateLicense(licenseKey, machineFingerprint, context);
isValid = isValid =
validationResponse != null validationResponse != null
&& validationResponse && validationResponse
@ -494,8 +498,8 @@ public class KeygenLicenseVerifier {
} }
} }
private JsonNode validateLicense(String licenseKey, String machineFingerprint, LicenseContext context) private JsonNode validateLicense(
throws Exception { String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
String requestBody = String requestBody =
String.format( String.format(
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}", "{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
@ -514,7 +518,8 @@ public class KeygenLicenseVerifier {
.POST(HttpRequest.BodyPublishers.ofString(requestBody)) .POST(HttpRequest.BodyPublishers.ofString(requestBody))
.build(); .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
log.info("ValidateLicenseResponse body: {}", response.body()); log.info("ValidateLicenseResponse body: {}", response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
@ -534,8 +539,10 @@ public class KeygenLicenseVerifier {
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false); context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1); context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
log.info("License floating (from license): {}, maxMachines: {}", log.info(
context.isFloatingLicense, context.maxMachines); "License floating (from license): {}, maxMachines: {}",
context.isFloatingLicense,
context.maxMachines);
} }
// Also check the policy for floating license support if included // Also check the policy for floating license support if included
@ -553,7 +560,8 @@ public class KeygenLicenseVerifier {
if (policyNode != null) { if (policyNode != null) {
// Check if this is a floating license from policy // Check if this is a floating license from policy
boolean policyFloating = policyNode.path("attributes").path("floating").asBoolean(false); boolean policyFloating =
policyNode.path("attributes").path("floating").asBoolean(false);
int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1); int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1);
// Policy takes precedence over license attributes // Policy takes precedence over license attributes
@ -562,8 +570,10 @@ public class KeygenLicenseVerifier {
context.maxMachines = policyMaxMachines; context.maxMachines = policyMaxMachines;
} }
log.info("License floating (from policy): {}, maxMachines: {}", log.info(
context.isFloatingLicense, context.maxMachines); "License floating (from policy): {}, maxMachines: {}",
context.isFloatingLicense,
context.maxMachines);
} }
// Extract user count, default to 1 if not specified // Extract user count, default to 1 if not specified
@ -593,11 +603,14 @@ public class KeygenLicenseVerifier {
return jsonResponse; return jsonResponse;
} }
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint, private boolean activateMachine(
LicenseContext context) throws Exception { String licenseKey, String licenseId, String machineFingerprint, LicenseContext context)
throws Exception {
// For floating licenses, we first need to check if we need to deregister any machines // For floating licenses, we first need to check if we need to deregister any machines
if (context.isFloatingLicense) { if (context.isFloatingLicense) {
log.info("Processing floating license activation. Max machines allowed: {}", context.maxMachines); log.info(
"Processing floating license activation. Max machines allowed: {}",
context.maxMachines);
// Get the current machines for this license // Get the current machines for this license
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId); JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
@ -605,17 +618,23 @@ public class KeygenLicenseVerifier {
JsonNode machines = machinesResponse.path("data"); JsonNode machines = machinesResponse.path("data");
int currentMachines = machines.size(); int currentMachines = machines.size();
log.info("Current machine count: {}, Max allowed: {}", currentMachines, context.maxMachines); log.info(
"Current machine count: {}, Max allowed: {}",
currentMachines,
context.maxMachines);
// Check if the current fingerprint is already activated // Check if the current fingerprint is already activated
boolean isCurrentMachineActivated = false; boolean isCurrentMachineActivated = false;
String currentMachineId = null; String currentMachineId = null;
for (JsonNode machine : machines) { for (JsonNode machine : machines) {
if (machineFingerprint.equals(machine.path("attributes").path("fingerprint").asText())) { if (machineFingerprint.equals(
machine.path("attributes").path("fingerprint").asText())) {
isCurrentMachineActivated = true; isCurrentMachineActivated = true;
currentMachineId = machine.path("id").asText(); currentMachineId = machine.path("id").asText();
log.info("Current machine is already activated with ID: {}", currentMachineId); log.info(
"Current machine is already activated with ID: {}",
currentMachineId);
break; break;
} }
} }
@ -628,7 +647,8 @@ public class KeygenLicenseVerifier {
// If we've reached the max machines limit, we need to deregister the oldest machine // If we've reached the max machines limit, we need to deregister the oldest machine
if (currentMachines >= context.maxMachines) { if (currentMachines >= context.maxMachines) {
log.info("Max machines reached. Deregistering oldest machine to make room for the new machine."); log.info(
"Max machines reached. Deregistering oldest machine to make room for the new machine.");
// Find the oldest machine based on creation timestamp // Find the oldest machine based on creation timestamp
if (machines.size() > 0) { if (machines.size() > 0) {
@ -637,23 +657,28 @@ public class KeygenLicenseVerifier {
java.time.Instant oldestTime = null; java.time.Instant oldestTime = null;
for (JsonNode machine : machines) { for (JsonNode machine : machines) {
String createdStr = machine.path("attributes").path("created").asText(null); String createdStr =
machine.path("attributes").path("created").asText(null);
if (createdStr != null && !createdStr.isEmpty()) { if (createdStr != null && !createdStr.isEmpty()) {
try { try {
java.time.Instant createdTime = java.time.Instant.parse(createdStr); java.time.Instant createdTime =
java.time.Instant.parse(createdStr);
if (oldestTime == null || createdTime.isBefore(oldestTime)) { if (oldestTime == null || createdTime.isBefore(oldestTime)) {
oldestTime = createdTime; oldestTime = createdTime;
oldestMachineId = machine.path("id").asText(); oldestMachineId = machine.path("id").asText();
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("Could not parse creation time for machine: {}", e.getMessage()); log.warn(
"Could not parse creation time for machine: {}",
e.getMessage());
} }
} }
} }
// If we couldn't determine the oldest by timestamp, use the first one // If we couldn't determine the oldest by timestamp, use the first one
if (oldestMachineId == null) { if (oldestMachineId == null) {
log.warn("Could not determine oldest machine by timestamp, using first machine in list"); log.warn(
"Could not determine oldest machine by timestamp, using first machine in list");
oldestMachineId = machines.path(0).path("id").asText(); oldestMachineId = machines.path(0).path("id").asText();
} }
@ -661,12 +686,15 @@ public class KeygenLicenseVerifier {
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId); boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
if (!deregistered) { if (!deregistered) {
log.error("Failed to deregister machine. Cannot proceed with activation."); log.error(
"Failed to deregister machine. Cannot proceed with activation.");
return false; return false;
} }
log.info("Machine deregistered successfully. Proceeding with activation of new machine."); log.info(
"Machine deregistered successfully. Proceeding with activation of new machine.");
} else { } else {
log.error("License has reached machine limit but no machines were found to deregister. This is unexpected."); log.error(
"License has reached machine limit but no machines were found to deregister. This is unexpected.");
// We'll still try to activate, but it might fail // We'll still try to activate, but it might fail
} }
} }
@ -720,7 +748,8 @@ public class KeygenLicenseVerifier {
.POST(HttpRequest.BodyPublishers.ofString(body.toString())) .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build(); .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
log.info("activateMachine Response body: " + response.body()); log.info("activateMachine Response body: " + response.body());
if (response.statusCode() == 201) { if (response.statusCode() == 201) {
log.info("Machine activated successfully"); log.info("Machine activated successfully");
@ -748,22 +777,33 @@ public class KeygenLicenseVerifier {
* @throws Exception if an error occurs during the HTTP request * @throws Exception if an error occurs during the HTTP request
*/ */
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception { private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request =
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/licenses/" + licenseId + "/machines")) HttpRequest.newBuilder()
.uri(
URI.create(
BASE_URL
+ "/"
+ ACCOUNT_ID
+ "/licenses/"
+ licenseId
+ "/machines"))
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
.header("Authorization", "License " + licenseKey) .header("Authorization", "License " + licenseKey)
.GET() .GET()
.build(); .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
log.info("fetchMachinesForLicense Response body: {}", response.body()); log.info("fetchMachinesForLicense Response body: {}", response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
return objectMapper.readTree(response.body()); return objectMapper.readTree(response.body());
} else { } else {
log.error("Error fetching machines for license. Status code: {}, error: {}", log.error(
response.statusCode(), response.body()); "Error fetching machines for license. Status code: {}, error: {}",
response.statusCode(),
response.body());
return null; return null;
} }
} }
@ -777,7 +817,8 @@ public class KeygenLicenseVerifier {
*/ */
private boolean deregisterMachine(String licenseKey, String machineId) { private boolean deregisterMachine(String licenseKey, String machineId) {
try { try {
HttpRequest request = HttpRequest.newBuilder() HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId)) .uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
@ -785,14 +826,17 @@ public class KeygenLicenseVerifier {
.DELETE() .DELETE()
.build(); .build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 204) { if (response.statusCode() == 204) {
log.info("Machine {} successfully deregistered", machineId); log.info("Machine {} successfully deregistered", machineId);
return true; return true;
} else { } else {
log.error("Error deregistering machine. Status code: {}, error: {}", log.error(
response.statusCode(), response.body()); "Error deregistering machine. Status code: {}, error: {}",
response.statusCode(),
response.body());
return false; return false;
} }
} catch (Exception e) { } catch (Exception e) {

View File

@ -1,18 +1,17 @@
package stirling.software.SPDF.service; package stirling.software.SPDF.service;
import java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.io.*; import java.io.*;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.Files;
import java.util.Arrays; import java.util.Arrays;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.*; import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDStream; import org.apache.pdfbox.pdmodel.common.PDStream;
import org.aspectj.lang.annotation.Before;
import org.apache.pdfbox.cos.COSName;
import org.junit.jupiter.api.*; import org.junit.jupiter.api.*;
import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.api.parallel.ExecutionMode;
@ -43,12 +42,7 @@ class CustomPDFDocumentFactoryTest {
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
"5,MEMORY_ONLY",
"20,MIXED",
"60,TEMP_FILE"
})
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException { void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB)); File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
try (PDDocument doc = factory.load(file)) { try (PDDocument doc = factory.load(file)) {
@ -57,12 +51,7 @@ class CustomPDFDocumentFactoryTest {
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
"5,MEMORY_ONLY",
"20,MIXED",
"60,TEMP_FILE"
})
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException { void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB); byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(inflated)) { try (PDDocument doc = factory.load(inflated)) {
@ -71,12 +60,7 @@ class CustomPDFDocumentFactoryTest {
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
"5,MEMORY_ONLY",
"20,MIXED",
"60,TEMP_FILE"
})
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException { void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB); byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) { try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
@ -85,30 +69,22 @@ class CustomPDFDocumentFactoryTest {
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
"5,MEMORY_ONLY",
"20,MIXED",
"60,TEMP_FILE"
})
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException { void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB); byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated); MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
try (PDDocument doc = factory.load(multipart)) { try (PDDocument doc = factory.load(multipart)) {
assertEquals(expected, factory.lastStrategyUsed); assertEquals(expected, factory.lastStrategyUsed);
} }
} }
@ParameterizedTest @ParameterizedTest
@CsvSource({ @CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
"5,MEMORY_ONLY",
"20,MIXED",
"60,TEMP_FILE"
})
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException { void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
byte[] inflated = inflatePdf(basePdfBytes, sizeMB); byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated); MockMultipartFile multipart =
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
PDFFile pdfFile = new PDFFile(); PDFFile pdfFile = new PDFFile();
pdfFile.setFileInput(multipart); pdfFile.setFileInput(multipart);
try (PDDocument doc = factory.load(pdfFile)) { try (PDDocument doc = factory.load(pdfFile)) {
@ -125,7 +101,9 @@ class CustomPDFDocumentFactoryTest {
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT); stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE); stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
doc.getDocumentCatalog().getCOSObject().setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject()); doc.getDocumentCatalog()
.getCOSObject()
.setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
doc.save(out); doc.save(out);
@ -151,28 +129,28 @@ class CustomPDFDocumentFactoryTest {
} }
// neeed to add password pdf // neeed to add password pdf
// @Test // @Test
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException { // void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) { // try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
// assertNotNull(is, "protected.pdf must be present in src/test/resources"); // assertNotNull(is, "protected.pdf must be present in src/test/resources");
// try (PDDocument doc = factory.load(is, "test123")) { // try (PDDocument doc = factory.load(is, "test123")) {
// assertNotNull(doc); // assertNotNull(doc);
// } // }
// } // }
// } // }
// //
// @Test // @Test
// void testLoadPasswordProtectedPdfFromMultipart() throws IOException { // void testLoadPasswordProtectedPdfFromMultipart() throws IOException {
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) { // try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
// assertNotNull(is, "protected.pdf must be present in src/test/resources"); // assertNotNull(is, "protected.pdf must be present in src/test/resources");
// byte[] bytes = is.readAllBytes(); // byte[] bytes = is.readAllBytes();
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf", "application/pdf", bytes); // MockMultipartFile file = new MockMultipartFile("file", "protected.pdf",
// try (PDDocument doc = factory.load(file, "test123")) { // "application/pdf", bytes);
// assertNotNull(doc); // try (PDDocument doc = factory.load(file, "test123")) {
// } // assertNotNull(doc);
// } // }
// } // }
// }
@Test @Test
void testLoadReadOnlySkipsPostProcessing() throws IOException { void testLoadReadOnlySkipsPostProcessing() throws IOException {
@ -186,7 +164,6 @@ class CustomPDFDocumentFactoryTest {
} }
} }
@Test @Test
void testCreateNewDocument() throws IOException { void testCreateNewDocument() throws IOException {
try (PDDocument doc = factory.createNewDocument()) { try (PDDocument doc = factory.createNewDocument()) {
@ -243,5 +220,4 @@ class CustomPDFDocumentFactoryTest {
void cleanup() { void cleanup() {
System.gc(); System.gc();
} }
} }

View File

@ -1,12 +1,12 @@
package stirling.software.SPDF.service; package stirling.software.SPDF.service;
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
import stirling.software.SPDF.service.CustomPDFDocumentFactory; import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
import stirling.software.SPDF.service.PdfMetadataService;
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory { class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
enum StrategyType { enum StrategyType {
MEMORY_ONLY, MIXED, TEMP_FILE MEMORY_ONLY,
MIXED,
TEMP_FILE
} }
public StrategyType lastStrategyUsed; public StrategyType lastStrategyUsed;

View File

@ -29,23 +29,26 @@ class CustomHtmlSanitizerTest {
return Stream.of( return Stream.of(
Arguments.of( Arguments.of(
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>", "<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
new String[] {"<p>", "<strong>", "<em>"} new String[] {"<p>", "<strong>", "<em>"}),
),
Arguments.of( Arguments.of(
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, " "<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, " + "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, " + "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>", + "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
new String[] {"<b>bold</b>", "<i>italic</i>", "<em>emphasis</em>", "<strong>strong</strong>"} new String[] {
), "<b>bold</b>",
"<i>italic</i>",
"<em>emphasis</em>",
"<strong>strong</strong>"
}),
Arguments.of( Arguments.of(
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>" "<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>" + "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>" + "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
+ "<ol><li>Ordered item</li></ol>", + "<ol><li>Ordered item</li></ol>",
new String[] {"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"} new String[] {
) "<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"
); }));
} }
@Test @Test

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.utils; package stirling.software.SPDF.utils;
import io.github.pixee.security.ZipSecurity;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -29,6 +28,8 @@ import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
/** /**
@ -214,7 +215,8 @@ class PDFToFileTest {
// Verify the content by unzipping it // Verify the content by unzipping it
try (ZipInputStream zipStream = try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) { ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry; ZipEntry entry;
boolean foundMdFiles = false; boolean foundMdFiles = false;
boolean foundImage = false; boolean foundImage = false;
@ -286,7 +288,8 @@ class PDFToFileTest {
// Verify the content by unzipping it // Verify the content by unzipping it
try (ZipInputStream zipStream = try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) { ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry; ZipEntry entry;
boolean foundMainHtml = false; boolean foundMainHtml = false;
boolean foundIndexHtml = false; boolean foundIndexHtml = false;
@ -437,7 +440,8 @@ class PDFToFileTest {
// Verify the content by unzipping it // Verify the content by unzipping it
try (ZipInputStream zipStream = try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) { ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry; ZipEntry entry;
boolean foundMainFile = false; boolean foundMainFile = false;
boolean foundMediaFiles = false; boolean foundMediaFiles = false;