Merge branch 'main' into scan-filter

This commit is contained in:
Balázs Szücs 2025-05-20 13:27:09 +02:00 committed by GitHub
commit 5791d61db3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 536 additions and 730 deletions

View File

@ -180,7 +180,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: .
file: ./Dockerfile

View File

@ -24,4 +24,4 @@ jobs:
- name: "Checkout Repository"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Dependency Review"
uses: actions/dependency-review-action@38ecb5b593bf0eb19e335c03f97670f792489a8b # v4.7.0
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1

View File

@ -38,7 +38,7 @@ jobs:
java-version: "17"
distribution: "adopt"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
- name: check the licenses for compatibility
run: ./gradlew clean checkLicense

View File

@ -68,7 +68,7 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
with:
gradle-version: 8.14
@ -156,7 +156,7 @@ jobs:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
with:
gradle-version: 8.14

View File

@ -30,7 +30,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
with:
gradle-version: 8.14
@ -90,7 +90,7 @@ jobs:
- name: Build and push main Dockerfile
id: build-push-regular
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
@ -135,7 +135,7 @@ jobs:
- name: Build and push Dockerfile-ultra-lite
id: build-push-lite
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
if: github.ref != 'refs/heads/main'
with:
context: .
@ -166,7 +166,7 @@ jobs:
- name: Build and push main Dockerfile fat
id: build-push-fat
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
if: github.ref != 'refs/heads/main'
with:
builder: ${{ steps.buildx.outputs.name }}

View File

@ -35,7 +35,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
with:
gradle-version: 8.14

View File

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

View File

@ -27,7 +27,7 @@ jobs:
fetch-depth: 0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
- name: Build and analyze with Gradle
env:

View File

@ -26,7 +26,7 @@ jobs:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
- name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs

View File

@ -46,7 +46,7 @@ jobs:
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push test image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: .
file: ./Dockerfile

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

@ -10,7 +10,7 @@ plugins {
id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.1"
id "org.sonarqube" version "6.1.0.5360"
id "org.sonarqube" version "6.2.0.5505"
}
import com.github.jk1.license.render.*
@ -24,7 +24,7 @@ ext {
imageioVersion = "3.12.0"
lombokVersion = "1.18.38"
bouncycastleVersion = "1.80"
springSecuritySamlVersion = "6.4.5"
springSecuritySamlVersion = "6.5.0"
openSamlVersion = "4.3.2"
tempJrePath = null
}
@ -434,7 +434,7 @@ dependencies {
}
//security updates
implementation "org.springframework:spring-webmvc:6.2.6"
implementation "org.springframework:spring-webmvc:6.2.7"
implementation("io.github.pixee:java-security-toolkit:1.2.1")
@ -459,7 +459,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion"
implementation "org.springframework.session:spring-session-core:3.4.3"
implementation "org.springframework:spring-jdbc:6.2.6"
implementation "org.springframework:spring-jdbc:6.2.7"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database
@ -528,7 +528,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.7"
implementation "io.micrometer:micrometer-core:1.15.0"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0"
@ -544,7 +544,7 @@ dependencies {
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
// Mockito (core)
testImplementation 'org.mockito:mockito-core:5.11.0'
testImplementation 'org.mockito:mockito-core:5.17.0'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'

View File

@ -1,5 +1,5 @@
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'Stirling-PDF'

View File

@ -49,7 +49,8 @@ public class KeygenLicenseVerifier {
private final ApplicationProperties applicationProperties;
// 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)
.connectTimeout(java.time.Duration.ofSeconds(10))
.build();
@ -416,7 +417,9 @@ public class KeygenLicenseVerifier {
if (policyFloating) {
context.isFloatingLicense = true;
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
@ -474,7 +477,8 @@ public class KeygenLicenseVerifier {
activateMachine(licenseKey, licenseId, machineFingerprint, context);
if (activated) {
// Revalidate after activation
validationResponse = validateLicense(licenseKey, machineFingerprint, context);
validationResponse =
validateLicense(licenseKey, machineFingerprint, context);
isValid =
validationResponse != null
&& validationResponse
@ -494,8 +498,8 @@ public class KeygenLicenseVerifier {
}
}
private JsonNode validateLicense(String licenseKey, String machineFingerprint, LicenseContext context)
throws Exception {
private JsonNode validateLicense(
String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
String requestBody =
String.format(
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
@ -514,7 +518,8 @@ public class KeygenLicenseVerifier {
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
.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());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
@ -534,8 +539,10 @@ public class KeygenLicenseVerifier {
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
log.info("License floating (from license): {}, maxMachines: {}",
context.isFloatingLicense, context.maxMachines);
log.info(
"License floating (from license): {}, maxMachines: {}",
context.isFloatingLicense,
context.maxMachines);
}
// Also check the policy for floating license support if included
@ -553,7 +560,8 @@ public class KeygenLicenseVerifier {
if (policyNode != null) {
// 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);
// Policy takes precedence over license attributes
@ -562,8 +570,10 @@ public class KeygenLicenseVerifier {
context.maxMachines = policyMaxMachines;
}
log.info("License floating (from policy): {}, maxMachines: {}",
context.isFloatingLicense, context.maxMachines);
log.info(
"License floating (from policy): {}, maxMachines: {}",
context.isFloatingLicense,
context.maxMachines);
}
// Extract user count, default to 1 if not specified
@ -593,11 +603,14 @@ public class KeygenLicenseVerifier {
return jsonResponse;
}
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint,
LicenseContext context) throws Exception {
private boolean activateMachine(
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
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
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
@ -605,17 +618,23 @@ public class KeygenLicenseVerifier {
JsonNode machines = machinesResponse.path("data");
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
boolean isCurrentMachineActivated = false;
String currentMachineId = null;
for (JsonNode machine : machines) {
if (machineFingerprint.equals(machine.path("attributes").path("fingerprint").asText())) {
if (machineFingerprint.equals(
machine.path("attributes").path("fingerprint").asText())) {
isCurrentMachineActivated = true;
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;
}
}
@ -628,7 +647,8 @@ public class KeygenLicenseVerifier {
// If we've reached the max machines limit, we need to deregister the oldest machine
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
if (machines.size() > 0) {
@ -637,23 +657,28 @@ public class KeygenLicenseVerifier {
java.time.Instant oldestTime = null;
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()) {
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)) {
oldestTime = createdTime;
oldestMachineId = machine.path("id").asText();
}
} 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 (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();
}
@ -661,12 +686,15 @@ public class KeygenLicenseVerifier {
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
if (!deregistered) {
log.error("Failed to deregister machine. Cannot proceed with activation.");
log.error(
"Failed to deregister machine. Cannot proceed with activation.");
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 {
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
}
}
@ -720,7 +748,8 @@ public class KeygenLicenseVerifier {
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.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());
if (response.statusCode() == 201) {
log.info("Machine activated successfully");
@ -748,22 +777,33 @@ public class KeygenLicenseVerifier {
* @throws Exception if an error occurs during the HTTP request
*/
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/licenses/" + licenseId + "/machines"))
HttpRequest request =
HttpRequest.newBuilder()
.uri(
URI.create(
BASE_URL
+ "/"
+ ACCOUNT_ID
+ "/licenses/"
+ licenseId
+ "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header("Authorization", "License " + licenseKey)
.GET()
.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());
if (response.statusCode() == 200) {
return objectMapper.readTree(response.body());
} else {
log.error("Error fetching machines for license. Status code: {}, error: {}",
response.statusCode(), response.body());
log.error(
"Error fetching machines for license. Status code: {}, error: {}",
response.statusCode(),
response.body());
return null;
}
}
@ -777,7 +817,8 @@ public class KeygenLicenseVerifier {
*/
private boolean deregisterMachine(String licenseKey, String machineId) {
try {
HttpRequest request = HttpRequest.newBuilder()
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
@ -785,14 +826,17 @@ public class KeygenLicenseVerifier {
.DELETE()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
HttpResponse<String> response =
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 204) {
log.info("Machine {} successfully deregistered", machineId);
return true;
} else {
log.error("Error deregistering machine. Status code: {}, error: {}",
response.statusCode(), response.body());
log.error(
"Error deregistering machine. Status code: {}, error: {}",
response.statusCode(),
response.body());
return false;
}
} catch (Exception e) {

View File

@ -31,7 +31,8 @@ public class LibreOfficeListener {
log.info("waiting for listener to start");
try (Socket socket = new Socket()) {
socket.connect(
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
new InetSocketAddress("localhost", LISTENER_PORT),
1000); // Timeout after 1 second
return true;
} catch (Exception e) {
return false;

View File

@ -11,8 +11,11 @@ import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
import org.thymeleaf.templateresource.FileTemplateResource;
import org.thymeleaf.templateresource.ITemplateResource;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.InputStreamTemplateResource;
@Slf4j
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
private final ResourceLoader resourceLoader;
@ -40,7 +43,8 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
}
} catch (IOException e) {
// Log the exception to help with debugging issues loading external templates
log.warn("Unable to read template '{}' from file system", resourceName, e);
}
InputStream inputStream =

View File

@ -93,6 +93,7 @@ public class PipelineProcessor {
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
boolean filtersApplied = false;
for (PipelineOperation pipelineOperation : config.getOperations()) {
String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
@ -134,7 +135,7 @@ public class PipelineProcessor {
if (operation.startsWith("filter-")
&& (response.getBody() == null
|| response.getBody().length == 0)) {
result.setFiltersApplied(true);
filtersApplied = true;
log.info("Skipping file due to filtering {}", operation);
continue;
}
@ -215,12 +216,12 @@ public class PipelineProcessor {
log.error("Errors occurred during processing. Log: {}", logStream.toString());
}
result.setHasErrors(hasErrors);
result.setFiltersApplied(hasErrors);
result.setFiltersApplied(filtersApplied);
result.setOutputFiles(outputFiles);
return result;
}
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
/* package */ ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
HttpHeaders headers = new HttpHeaders();

View File

@ -77,9 +77,8 @@ public class HomeWebController {
}
@GetMapping("/home-legacy")
public String homeLegacy(Model model) {
model.addAttribute("currentPage", "home-legacy");
return "home-legacy";
public String redirectHomeLegacy() {
return "redirect:/";
}
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)

View File

@ -39,7 +39,6 @@ public class InputStreamTemplateResource implements ITemplateResource {
@Override
public boolean exists() {
// TODO Auto-generated method stub
return false;
return inputStream != null;
}
}

View File

@ -553,7 +553,7 @@
{
"moduleName": "io.micrometer:micrometer-core",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
"moduleVersion": "1.14.7",
"moduleVersion": "1.15.0",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
@ -1637,7 +1637,7 @@
{
"moduleName": "org.springframework.security:spring-security-saml2-service-provider",
"moduleUrl": "https://spring.io/projects/spring-security",
"moduleVersion": "6.4.5",
"moduleVersion": "6.5.0",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@ -1714,7 +1714,7 @@
{
"moduleName": "org.springframework:spring-jdbc",
"moduleUrl": "https://github.com/spring-projects/spring-framework",
"moduleVersion": "6.2.6",
"moduleVersion": "6.2.7",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
@ -1742,7 +1742,7 @@
{
"moduleName": "org.springframework:spring-webmvc",
"moduleUrl": "https://github.com/spring-projects/spring-framework",
"moduleVersion": "6.2.6",
"moduleVersion": "6.2.7",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},

View File

@ -1,229 +0,0 @@
#searchBar {
color: var(--md-sys-color-on-surface);
background-color: var(--md-sys-color-surface-container-low);
width: 100%;
font-size: 16px;
margin-bottom: 2rem;
padding: 0.75rem 3.5rem;
border: 1px solid var(--md-sys-color-outline-variant);
border-radius: 3rem;
outline-color: var(--md-sys-color-outline-variant);
}
#filtersContainer {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
gap: 10px;
}
.filter-button {
color: var(--md-sys-color-secondary);
user-select: none;
cursor: pointer;
transition: transform 0.3s;
transform-origin: center center;
}
.filter-button:hover {
transform: scale(1.08);
}
.search-icon {
position: absolute;
margin: 0.75rem 1rem;
border: 0.1rem solid transparent;
}
.features-container {
display: flex;
flex-direction: column;
gap: 30px;
}
.feature-group-legacy {
display: flex;
flex-direction: column;
}
.feature-group-header {
display: flex;
align-items: center;
justify-content: flex-start;
color: var(--md-sys-color-on-surface);
margin-bottom: 15px;
user-select: none;
cursor: pointer;
gap: 10px;
}
.feature-group-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr));
gap: 30px 30px;
overflow: hidden;
margin: -20px;
padding: 20px;
box-sizing:content-box;
}
.feature-group-container.animated-group {
transition: 0.5s all;
}
.feature-group-legacy.collapsed>.feature-group-container {
max-height: 0 !important;
margin: 0;
padding: 0;
}
.header-expand-button {
transition: 0.5s all;
transform: rotate(90deg);
}
.header-expand-button.collapsed {
transform: rotate(0deg);
}
.feature-card {
border: 1px solid var(--md-sys-color-surface-5);
border-radius: 1.75rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
align-items: flex-start;
background: var(--md-sys-color-surface-5);
transition:
transform 0.3s,
border 0.3s;
transform-origin: center center;
outline: 0px solid transparent;
position:relative;
}
.feature-card a {
text-decoration: none;
color: var(--md-sys-color-on-surface);
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.feature-card .card-text {
font-size: .875rem;
}
.feature-card:hover {
cursor: pointer;
transform: scale(1.08);
box-shadow: var(--md-sys-elevation-2);
}
.card-title.text-primary {
color: #000;
}
.home-card-icon {
width: 3rem;
height: 3rem;
transform: translateY(-5px);
}
.favorite-icon {
display: none !important;
position: absolute;
top: 10px;
right: 10px;
color: var(--md-sys-color-secondary);
}
#tool-icon {
height: 100%;
}
#tool-text {
margin: 0.0rem 0 0 1.25rem;
}
.card-title {
margin-bottom: 1rem;
font-size: 1.1rem;
}
/* Only show the favorite icons when the parent card is being hovered over */
.feature-card:hover .favorite-icon {
display: block !important;
}
.favorite-icon img {
filter: brightness(0) invert(var(--md-theme-filter-color));
}
.favorite-icon:hover .material-symbols-rounded {
transform: scale(1.2);
}
.favorite-icon .material-symbols-rounded.fill{
color: #f5c000;
}
.jumbotron {
padding: 3rem 3rem;
/* Reduce vertical padding */
}
.lookatme {
opacity: 1;
position: relative;
display: inline-block;
}
.lookatme::after {
color: #e33100;
text-shadow: 0 0 5px #e33100;
/* in the html, the data-lookatme-text attribute must */
/* contain the same text as the .lookatme element */
content: attr(data-lookatme-text);
padding: inherit;
position: absolute;
inset: 0 0 0 0;
z-index: 1;
/* 20 steps / 2 seconds = 10fps */
-webkit-animation: 2s infinite Pulse steps(20);
animation: 2s infinite Pulse steps(20);
}
@keyframes Pulse {
from {
opacity: 0;
}
50% {
opacity: 1;
}
to {
opacity: 0;
}
}
.update-notice {
animation: scale 1s infinite alternate;
}
@keyframes scale {
0% {
transform: scale(0.96);
}
100% {
transform: scale(1);
}
}
.hidden {
visibility: hidden;
}

View File

@ -126,11 +126,7 @@ function addToFavorites(entryId) {
localStorage.setItem('favoritesList', JSON.stringify(favoritesList));
updateFavoritesDropdown();
updateFavoriteIcons();
const currentPath = window.location.pathname;
if (currentPath.includes('home-legacy')) {
syncFavoritesLegacy();
} else {
initializeCards();
}
}
}

View File

@ -1,266 +0,0 @@
function filterCardsLegacy() {
var input = document.getElementById('searchBar');
var filter = input.value.toUpperCase();
let featureGroups = document.querySelectorAll('.feature-group-legacy');
const collapsedGroups = getCollapsedGroups();
for (const featureGroup of featureGroups) {
var cards = featureGroup.querySelectorAll('.feature-card');
let groupMatchesFilter = false;
for (var i = 0; i < cards.length; i++) {
var card = cards[i];
var title = card.querySelector('h5.card-title').innerText;
var text = card.querySelector('p.card-text').innerText;
// Get the navbar tags associated with the card
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : '';
var content = title + ' ' + text + ' ' + navbarTags;
if (content.toUpperCase().indexOf(filter) > -1) {
card.style.display = '';
groupMatchesFilter = true;
} else {
card.style.display = 'none';
}
}
if (!groupMatchesFilter) {
featureGroup.style.display = 'none';
} else {
featureGroup.style.display = '';
resetOrTemporarilyExpandGroup(featureGroup, filter, collapsedGroups);
}
}
}
function getCollapsedGroups() {
return localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
}
function resetOrTemporarilyExpandGroup(featureGroup, filterKeywords = '', collapsedGroups = []) {
const shouldResetCollapse = filterKeywords.trim() === '';
if (shouldResetCollapse) {
// Resetting the group's expand/collapse to its original state (as in collapsed groups)
const isCollapsed = collapsedGroups.indexOf(featureGroup.id) != -1;
expandCollapseToggle(featureGroup, !isCollapsed);
} else {
// Temporarily expands feature group without affecting the actual/stored collapsed groups
featureGroup.classList.remove('collapsed');
featureGroup.querySelector('.header-expand-button').classList.remove('collapsed');
}
}
function updateFavoritesSectionLegacy() {
const favoritesContainer = document.getElementById('groupFavorites').querySelector('.feature-group-container');
favoritesContainer.innerHTML = '';
const cards = Array.from(document.querySelectorAll('.feature-card:not(.duplicate)'));
const addedCardIds = new Set();
let favoritesAmount = 0;
cards.forEach((card) => {
const favouritesList = JSON.parse(localStorage.getItem('favoritesList') || '[]');
if (favouritesList.includes(card.id) && !addedCardIds.has(card.id)) {
const duplicate = card.cloneNode(true);
duplicate.classList.add('duplicate');
favoritesContainer.appendChild(duplicate);
addedCardIds.add(card.id);
favoritesAmount++;
}
});
if (favoritesAmount === 0) {
document.getElementById('groupFavorites').style.display = 'none';
} else {
document.getElementById('groupFavorites').style.display = 'flex';
}
reorderCards(favoritesContainer);
}
function syncFavoritesLegacy() {
const cards = Array.from(document.querySelectorAll('.feature-card'));
cards.forEach((card) => {
const isFavorite = localStorage.getItem(card.id) === 'favorite';
const starIcon = card.querySelector('.favorite-icon span.material-symbols-rounded');
if (starIcon) {
if (isFavorite) {
starIcon.classList.remove('no-fill');
starIcon.classList.add('fill');
card.classList.add('favorite');
} else {
starIcon.classList.remove('fill');
starIcon.classList.add('no-fill');
card.classList.remove('favorite');
}
}
});
updateFavoritesSectionLegacy();
updateFavoritesDropdown();
filterCardsLegacy();
}
function reorderCards(container) {
var cards = Array.from(container.querySelectorAll('.feature-card'));
cards.forEach(function (card) {
container.removeChild(card);
});
cards.sort(function (a, b) {
var aIsFavorite = localStorage.getItem(a.id) === 'favorite';
var bIsFavorite = localStorage.getItem(b.id) === 'favorite';
if (a.id === 'update-link') {
return -1;
}
if (b.id === 'update-link') {
return 1;
}
if (aIsFavorite && !bIsFavorite) {
return -1;
} else if (!aIsFavorite && bIsFavorite) {
return 1;
} else {
return a.id > b.id;
}
});
cards.forEach(function (card) {
container.appendChild(card);
});
}
function reorderAllCards() {
const containers = Array.from(document.querySelectorAll('.feature-group-container'));
containers.forEach(function (container) {
reorderCards(container);
});
}
function initializeCardsLegacy() {
reorderAllCards();
updateFavoritesSectionLegacy();
updateFavoritesDropdown();
filterCardsLegacy();
}
function showFavoritesOnly() {
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
if (localStorage.getItem('favoritesOnly') === 'true') {
groups.forEach((group) => {
if (group.id !== 'groupFavorites') {
group.style.display = 'none';
}
});
} else {
groups.forEach((group) => {
if (group.id !== 'groupFavorites') {
group.style.display = 'flex';
}
});
}
}
function toggleFavoritesOnly() {
if (localStorage.getItem('favoritesOnly') === 'true') {
localStorage.setItem('favoritesOnly', 'false');
} else {
localStorage.setItem('favoritesOnly', 'true');
}
showFavoritesOnly();
}
// Expands a feature group on true, collapses it on false and toggles state on null.
function expandCollapseToggle(group, expand = null) {
if (expand === null) {
group.classList.toggle('collapsed');
group.querySelector('.header-expand-button').classList.toggle('collapsed');
} else if (expand) {
group.classList.remove('collapsed');
group.querySelector('.header-expand-button').classList.remove('collapsed');
} else {
group.classList.add('collapsed');
group.querySelector('.header-expand-button').classList.add('collapsed');
}
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
const groupIndex = collapsed.indexOf(group.id);
if (group.classList.contains('collapsed')) {
if (groupIndex === -1) {
collapsed.push(group.id);
}
} else {
if (groupIndex !== -1) {
collapsed.splice(groupIndex, 1);
}
}
localStorage.setItem('collapsedGroups', JSON.stringify(collapsed));
}
function expandCollapseAll(expandAll) {
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
groups.forEach((group) => {
expandCollapseToggle(group, expandAll);
});
}
window.onload = function () {
initializeCardsLegacy();
syncFavoritesLegacy(); // Ensure everything is in sync on page load
};
document.addEventListener('DOMContentLoaded', function () {
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
materialIcons
.load()
.then(() => {
document.querySelectorAll('.feature-card.hidden').forEach((el) => {
el.classList.remove('hidden');
});
})
.catch(() => {
console.error('Material Symbols Rounded font failed to load.');
});
Array.from(document.querySelectorAll('.feature-group-header-legacy')).forEach((header) => {
const parent = header.parentNode;
const container = header.parentNode.querySelector('.feature-group-container');
if (parent.id !== 'groupFavorites') {
// container.style.maxHeight = container.scrollHeight + 'px';
}
header.onclick = () => {
expandCollapseToggle(parent);
};
});
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
const groupsArray = Array.from(document.querySelectorAll('.feature-group-legacy'));
groupsArray.forEach((group) => {
if (collapsed.indexOf(group.id) !== -1) {
expandCollapseToggle(group, false);
}
});
// Necessary in order to not fire the transition animation on page load, which looks wrong.
// The timeout isn't doing anything visible to the user, so it's not making the page load look slower.
setTimeout(() => {
groupsArray.forEach((group) => {
const container = group.querySelector('.feature-group-container');
container.classList.add('animated-group');
});
}, 500);
Array.from(document.querySelectorAll('.feature-group-header')).forEach((header) => {
const parent = header.parentNode;
header.onclick = () => {
expandCollapseToggle(parent);
};
});
showFavoritesOnly();
});

View File

@ -55,10 +55,6 @@ hideCookieBanner();
updateFavoriteIcons();
const contentPath = /*[[${@contextPath}]]*/ '';
const defaultView = localStorage.getItem('defaultView') || 'home'; // Default to "home"
if (defaultView === 'home-legacy') {
window.location.href = contentPath + 'home-legacy'; // Redirect to legacy view
}
document.addEventListener('DOMContentLoaded', function () {
const surveyVersion = '3.0';

View File

@ -1,6 +0,0 @@
<div th:fragment="featureGroupHeader" class="feature-group-header">
<h3 class="menu-title" th:text="${groupTitle}"></h3>
<span class="material-symbols-rounded header-expand-button">
chevron_right
</span>
</div>

View File

@ -82,13 +82,6 @@
visibility
</span>
</div>
<a href="home" onclick="setAsDefault('home-legacy')" th:title="#{home.legacyHomepage}"
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
<span class="material-symbols-rounded toggle-favourites"
style="font-size: 2rem; margin-left: 0.2rem;">
home
</span>
</a>
<a th:if="${@shouldShow}" href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
target="_blank" id="update-link" rel="noopener" th:title="#{settings.update}"
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">

View File

@ -0,0 +1,77 @@
package stirling.software.SPDF.EE;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
import stirling.software.SPDF.model.ApplicationProperties;
@ExtendWith(MockitoExtension.class)
class LicenseKeyCheckerTest {
@Mock private KeygenLicenseVerifier verifier;
@Test
void premiumDisabled_skipsVerification() {
ApplicationProperties props = new ApplicationProperties();
props.getPremium().setEnabled(false);
props.getPremium().setKey("dummy");
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
assertEquals(License.NORMAL, checker.getPremiumLicenseEnabledResult());
verifyNoInteractions(verifier);
}
@Test
void directKey_verified() {
ApplicationProperties props = new ApplicationProperties();
props.getPremium().setEnabled(true);
props.getPremium().setKey("abc");
when(verifier.verifyLicense("abc")).thenReturn(License.PRO);
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
assertEquals(License.PRO, checker.getPremiumLicenseEnabledResult());
verify(verifier).verifyLicense("abc");
}
@Test
void fileKey_verified(@TempDir Path temp) throws IOException {
Path file = temp.resolve("license.txt");
Files.writeString(file, "filekey");
ApplicationProperties props = new ApplicationProperties();
props.getPremium().setEnabled(true);
props.getPremium().setKey("file:" + file.toString());
when(verifier.verifyLicense("filekey")).thenReturn(License.ENTERPRISE);
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
assertEquals(License.ENTERPRISE, checker.getPremiumLicenseEnabledResult());
verify(verifier).verifyLicense("filekey");
}
@Test
void missingFile_resultsNormal(@TempDir Path temp) {
Path file = temp.resolve("missing.txt");
ApplicationProperties props = new ApplicationProperties();
props.getPremium().setEnabled(true);
props.getPremium().setKey("file:" + file.toString());
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
assertEquals(License.NORMAL, checker.getPremiumLicenseEnabledResult());
verifyNoInteractions(verifier);
}
}

View File

@ -0,0 +1,76 @@
package stirling.software.SPDF.controller.api.pipeline;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import jakarta.servlet.ServletContext;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.PipelineResult;
@ExtendWith(MockitoExtension.class)
class PipelineProcessorTest {
@Mock
ApiDocService apiDocService;
@Mock
UserServiceInterface userService;
@Mock
ServletContext servletContext;
PipelineProcessor pipelineProcessor;
@BeforeEach
void setUp() {
pipelineProcessor = spy(new PipelineProcessor(apiDocService, userService, servletContext));
}
@Test
void runPipelineWithFilterSetsFlag() throws Exception {
PipelineOperation op = new PipelineOperation();
op.setOperation("filter-page-count");
op.setParameters(Map.of());
PipelineConfig config = new PipelineConfig();
config.setOperations(List.of(op));
Resource file = new ByteArrayResource("data".getBytes()) {
@Override
public String getFilename() {
return "test.pdf";
}
};
List<Resource> files = List.of(file);
when(apiDocService.isMultiInput("filter-page-count")).thenReturn(false);
when(apiDocService.getExtensionTypes(false, "filter-page-count")).thenReturn(List.of("pdf"));
doReturn(new ResponseEntity<>(new byte[0], HttpStatus.OK))
.when(pipelineProcessor)
.sendWebRequest(anyString(), any());
PipelineResult result = pipelineProcessor.runPipelineAgainstFiles(files, config);
assertTrue(result.isFiltersApplied(), "Filter flag should be true when operation filters file");
assertFalse(result.isHasErrors(), "No errors should occur");
assertTrue(result.getOutputFiles().isEmpty(), "Filtered file list should be empty");
}
}

View File

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

View File

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

View File

@ -29,23 +29,26 @@ class CustomHtmlSanitizerTest {
return Stream.of(
Arguments.of(
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
new String[] {"<p>", "<strong>", "<em>"}
),
new String[] {"<p>", "<strong>", "<em>"}),
Arguments.of(
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
+ "<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(
"<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>"
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
+ "<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

View File

@ -0,0 +1,41 @@
package stirling.software.SPDF.utils;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class GeneralUtilsAdditionalTest {
@Test
void testConvertSizeToBytes() {
assertEquals(1024L, GeneralUtils.convertSizeToBytes("1KB"));
assertEquals(1024L * 1024, GeneralUtils.convertSizeToBytes("1MB"));
assertEquals(1024L * 1024 * 1024, GeneralUtils.convertSizeToBytes("1GB"));
assertEquals(100L * 1024 * 1024, GeneralUtils.convertSizeToBytes("100"));
assertNull(GeneralUtils.convertSizeToBytes("invalid"));
assertNull(GeneralUtils.convertSizeToBytes(null));
}
@Test
void testFormatBytes() {
assertEquals("512 B", GeneralUtils.formatBytes(512));
assertEquals("1.00 KB", GeneralUtils.formatBytes(1024));
assertEquals("1.00 MB", GeneralUtils.formatBytes(1024L * 1024));
assertEquals("1.00 GB", GeneralUtils.formatBytes(1024L * 1024 * 1024));
}
@Test
void testURLHelpersAndUUID() {
assertTrue(GeneralUtils.isValidURL("https://example.com"));
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
assertFalse(GeneralUtils.isValidUUID("not-a-uuid"));
assertFalse(GeneralUtils.isVersionHigher(null, "1.0"));
assertTrue(GeneralUtils.isVersionHigher("2.0", "1.9"));
assertFalse(GeneralUtils.isVersionHigher("1.0", "1.0.1"));
}
}

View File

@ -1,6 +1,5 @@
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.assertNotNull;
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.web.multipart.MultipartFile;
import io.github.pixee.security.ZipSecurity;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
/**
@ -214,7 +215,8 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMdFiles = false;
boolean foundImage = false;
@ -286,7 +288,8 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMainHtml = false;
boolean foundIndexHtml = false;
@ -437,7 +440,8 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
ZipEntry entry;
boolean foundMainFile = false;
boolean foundMediaFiles = false;

View File

@ -5,12 +5,17 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
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.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
@ -18,6 +23,10 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.PdfMetadataService;
public class PdfUtilsTest {
@Test
@ -49,4 +58,68 @@ public class PdfUtilsTest {
assertTrue(PdfUtils.hasImagesOnPage(page));
}
@Test
void testPageCountComparators() throws Exception {
PDDocument doc1 = new PDDocument();
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
doc1.addPage(new PDPage());
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageCount(doc1, 2, "greater"));
PDDocument doc2 = new PDDocument();
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
doc2.addPage(new PDPage());
assertTrue(utils.pageCount(doc2, 3, "equal"));
PDDocument doc3 = new PDDocument();
doc3.addPage(new PDPage());
doc3.addPage(new PDPage());
assertTrue(utils.pageCount(doc3, 5, "less"));
PDDocument doc4 = new PDDocument();
doc4.addPage(new PDPage());
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
}
@Test
void testPageSize() throws Exception {
PDDocument doc = new PDDocument();
PDPage page = new PDPage(PDRectangle.A4);
doc.addPage(page);
PDRectangle rect = page.getMediaBox();
String expected = rect.getWidth() + "x" + rect.getHeight();
PdfUtils utils = new PdfUtils();
assertTrue(utils.pageSize(doc, expected));
}
@Test
void testOverlayImage() throws Exception {
PDDocument doc = new PDDocument();
doc.addPage(new PDPage(PDRectangle.A4));
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
doc.save(pdfOut);
doc.close();
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.RED);
g.fillRect(0, 0, 10, 10);
g.dispose();
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(image, "png", imgOut);
PdfMetadataService meta =
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
byte[] result =
PdfUtils.overlayImage(
factory, pdfOut.toByteArray(), imgOut.toByteArray(), 0, 0, false);
try (PDDocument resultDoc = factory.load(result)) {
assertEquals(1, resultDoc.getNumberOfPages());
}
}
}