Compare commits

..
Author SHA1 Message Date
Anthony Stirling e68a24fc07 Update README.md 2026-02-25 19:00:36 +00:00
13 changed files with 17 additions and 255 deletions
+1 -69
View File
@@ -1,69 +1 @@
<p align="center">
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" alt="Stirling PDF logo">
</p>
<h1 align="center">Stirling PDF - The Open-Source PDF Platform</h1>
Stirling PDF is a powerful, open-source PDF editing platform. Run it as a personal desktop app, in the browser, or deploy it on your own servers with a private API. Edit, sign, redact, convert, and automate PDFs without sending documents to external services.
<p align="center">
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf">
<img src="https://img.shields.io/docker/pulls/frooodle/s-pdf" alt="Docker Pulls">
</a>
<a href="https://discord.gg/HYmhKj45pU">
<img src="https://img.shields.io/discord/1068636748814483718?label=Discord" alt="Discord">
</a>
<a href="https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF">
<img src="https://api.scorecard.dev/projects/github.com/Stirling-Tools/Stirling-PDF/badge" alt="OpenSSF Scorecard">
</a>
<a href="https://github.com/Stirling-Tools/stirling-pdf">
<img src="https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social" alt="GitHub Repo stars">
</a>
</p>
![Stirling PDF - Dashboard](images/home-light.png)
## Key Capabilities
- **Everywhere you work** - Desktop client, browser UI, and self-hosted server with a private API.
- **50+ PDF tools** - Edit, merge, split, sign, redact, convert, OCR, compress, and more.
- **Automation & workflows** - No-code pipelines direct in UI with APIs to process millions of PDFs.
- **Enterprisegrade** - SSO, auditing, and flexible onprem deployments.
- **Developer platform** - REST APIs available for nearly all tools to integrate into your existing systems.
- **Global UI** - Interface available in 40+ languages.
For a full feature list, see the docs: **https://docs.stirlingpdf.com**
## Quick Start
```bash
docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf
```
Then open: http://localhost:8080
For full installation options (including desktop and Kubernetes), see our [Documentation Guide](https://docs.stirlingpdf.com/#documentation-guide).
## Resources
- [**Documentation**](https://docs.stirlingpdf.com)
- [**Homepage**](https://stirling.com)
- [**API Docs**](https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/)
- [**Server Plan & Enterprise**](https://docs.stirlingpdf.com/Paid-Offerings)
## Support
- **Community** [Discord](https://discord.gg/HYmhKj45pU)
- **Bug Reports**: [Github issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
## Contributing
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
For development setup, see the [Developer Guide](DeveloperGuide.md).
For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md).
## License
Stirling PDF is open-core. See [LICENSE](LICENSE) for details.
dont emrge only for test delpoyment
+2 -2
View File
@@ -29,7 +29,7 @@ spotless {
dependencies {
api 'org.springframework.boot:spring-boot-starter-webmvc'
api 'org.springframework.boot:spring-boot-starter-aspectj'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20260102.1'
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
api 'com.fathzer:javaluator:3.0.6'
api 'com.posthog.java:posthog:1.2.0'
api 'org.apache.commons:commons-lang3:3.20.0'
@@ -39,7 +39,7 @@ dependencies {
api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion"
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
api "org.apache.pdfbox:preflight:$pdfboxVersion"
api 'com.github.junrar:junrar:7.5.8' // RAR archive support for CBR files
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
api 'org.snakeyaml:snakeyaml-engine:3.0.1'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1"
@@ -169,10 +169,7 @@ public class PdfMetadataService {
.getAuthor();
if (userService != null) {
String username = userService.getCurrentUsername();
if (username != null) {
author = author.replace("username", username);
}
author = author.replace("username", userService.getCurrentUsername());
}
}
pdf.getDocumentInformation().setAuthor(author);
+2 -2
View File
@@ -77,7 +77,7 @@ dependencies {
implementation 'org.verapdf:validation-model:1.28.2'
// CVE-2025-66453: Explicit rhino 1.7.15 to override verapdf's 1.7.13
implementation 'org.mozilla:rhino:1.9.1'
implementation 'org.mozilla:rhino:1.7.15'
// veraPDF still uses javax.xml.bind, not the new jakarta namespace
implementation 'javax.xml.bind:jaxb-api:2.3.1'
@@ -92,7 +92,7 @@ dependencies {
exclude group: 'com.google.code.gson', module: 'gson'
}
// CVE-2022-25647: Explicit gson 2.8.9 to prevent unsafe deserialization (tabula would pull 2.8.7)
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation 'com.opencsv:opencsv:5.12.0' // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation 'org.apache.poi:poi-ooxml:5.5.1'
@@ -168,8 +168,7 @@ public class UIDataController {
public ResponseEntity<SignData> getSignData() {
String username = "";
if (userService != null) {
String currentUsername = userService.getCurrentUsername();
username = currentUsername != null ? currentUsername : "";
username = userService.getCurrentUsername();
}
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
@@ -191,30 +191,6 @@ class PdfMetadataServiceTest {
// We don't verify setProducer here to avoid the "Too many actual invocations" error
}
@Test
void testSetMetadataToPdf_WithProFeaturesAndMissingUsername() {
PDDocument testDocument = mock(PDDocument.class);
PDDocumentInformation testInfo = mock(PDDocumentInformation.class);
when(testDocument.getDocumentInformation()).thenReturn(testInfo);
PdfMetadataService proService =
new PdfMetadataService(
applicationProperties, STIRLING_PDF_LABEL, true, userService);
PdfMetadata testMetadata = PdfMetadata.builder().author("Original Author").build();
CustomMetadata customMetadata =
applicationProperties.getPremium().getProFeatures().getCustomMetadata();
when(customMetadata.isAutoUpdateMetadata()).thenReturn(true);
when(customMetadata.getAuthor()).thenReturn("Pro Author username");
when(userService.getCurrentUsername()).thenReturn(null);
proService.setMetadataToPdf(testDocument, testMetadata, false);
// When username is null, the "username" placeholder should not be replaced
verify(testInfo).setAuthor("Pro Author username");
}
@Test
void testSetMetadataToPdf_ExistingDocument() {
// Create a fresh document
+4 -4
View File
@@ -49,20 +49,20 @@ dependencies {
api 'org.springframework.boot:spring-boot-starter-mail'
api 'org.springframework.boot:spring-boot-starter-cache'
api 'com.github.ben-manes.caffeine:caffeine'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.43'
implementation 'com.bucket4j:bucket4j_jdk17-core:8.16.1'
api 'io.swagger.core.v3:swagger-core-jakarta:2.2.42'
implementation 'com.bucket4j:bucket4j_jdk17-core:8.15.0'
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
api 'io.micrometer:micrometer-registry-prometheus'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:5.0.0'
implementation 'com.unboundid.product.scim2:scim2-sdk-client:4.1.0'
api "io.jsonwebtoken:jjwt-api:$jwtVersion"
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database
runtimeOnly 'org.postgresql:postgresql:42.7.10'
runtimeOnly 'org.postgresql:postgresql:42.7.9'
constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
@@ -48,19 +48,11 @@ public class SignatureController {
* requirements.
*/
@PostMapping
@PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")
public ResponseEntity<SavedSignatureResponse> saveSignature(
@RequestBody SavedSignatureRequest request) {
try {
String username = userService.getCurrentUsername();
if ("shared".equals(request.getScope()) && !userService.isCurrentUserAdmin()) {
log.warn(
"User {} attempted to create shared signature without admin role",
username);
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// Validate request
if (request.getDataUrl() == null || request.getDataUrl().isEmpty()) {
log.warn("User {} attempted to save signature without dataUrl", username);
@@ -84,7 +76,6 @@ public class SignatureController {
* signatures.
*/
@GetMapping
@PreAuthorize("isAuthenticated() && !hasAuthority('ROLE_DEMO_USER')")
public ResponseEntity<List<SavedSignatureResponse>> listSignatures() {
try {
String username = userService.getCurrentUsername();
@@ -107,21 +98,12 @@ public class SignatureController {
try {
String username = userService.getCurrentUsername();
String newLabel = body.get("label");
boolean isAdmin = userService.isCurrentUserAdmin();
if (newLabel == null || newLabel.trim().isEmpty()) {
log.warn("Invalid label update request");
return ResponseEntity.badRequest().build();
}
if (signatureService.isSharedSignature(signatureId) && !isAdmin) {
log.warn(
"User {} attempted to update shared signature {} without admin role",
username,
signatureId);
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
signatureService.updateSignatureLabel(username, signatureId, newLabel);
log.info("User {} updated label for signature {}", username, signatureId);
return ResponseEntity.noContent().build();
@@ -522,17 +522,7 @@ public class UserService implements UserServiceInterface {
@Override
public String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
log.debug("No authentication found in security context when resolving username");
return null;
}
Object principal = authentication.getPrincipal();
if (principal == null) {
log.debug("No principal found on authentication object when resolving username");
return null;
}
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails detailsUser) {
return detailsUser.getUsername();
@@ -249,12 +249,6 @@ public class SignatureService implements PersonalSignatureServiceInterface {
throw new FileNotFoundException("Signature metadata not found");
}
public boolean isSharedSignature(String signatureId) {
validateFileName(signatureId);
Path sharedFolder = Paths.get(SIGNATURE_BASE_PATH, ALL_USERS_FOLDER);
return Files.exists(sharedFolder.resolve(signatureId + ".json"));
}
private void updateMetadataLabel(Path metadataPath, String newLabel) throws IOException {
String metadataJson = Files.readString(metadataPath, StandardCharsets.UTF_8);
SavedSignatureResponse sig =
@@ -1,87 +0,0 @@
package stirling.software.proprietary.controller.api;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.service.SignatureService;
@ExtendWith(MockitoExtension.class)
class SignatureControllerTest {
@Mock private SignatureService signatureService;
@Mock private UserService userService;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
SignatureController controller = new SignatureController(signatureService, userService);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
@Test
void saveSignatureForbidsSharedScopeForNonAdmin() throws Exception {
when(userService.getCurrentUsername()).thenReturn("user1");
when(userService.isCurrentUserAdmin()).thenReturn(false);
mockMvc.perform(
post("/api/v1/proprietary/signatures")
.contentType(MediaType.APPLICATION_JSON)
.content(
"""
{
"id": "sig1",
"scope": "shared",
"dataUrl": "data:image/png;base64,AAAA"
}
"""))
.andExpect(status().isForbidden());
verify(signatureService, never()).saveSignature(any(), any());
}
@Test
void updateSignatureLabelForbidsSharedSignatureForNonAdmin() throws Exception {
when(userService.getCurrentUsername()).thenReturn("user1");
when(userService.isCurrentUserAdmin()).thenReturn(false);
when(signatureService.isSharedSignature("sig123")).thenReturn(true);
mockMvc.perform(
post("/api/v1/proprietary/signatures/sig123/label")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"label\":\"new label\"}"))
.andExpect(status().isForbidden());
verify(signatureService, never()).updateSignatureLabel(any(), any(), any());
}
@Test
void updateSignatureLabelAllowsPersonalSignatureForNonAdmin() throws Exception {
when(userService.getCurrentUsername()).thenReturn("user1");
when(userService.isCurrentUserAdmin()).thenReturn(false);
when(signatureService.isSharedSignature("sig123")).thenReturn(false);
mockMvc.perform(
post("/api/v1/proprietary/signatures/sig123/label")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"label\":\"new label\"}"))
.andExpect(status().isNoContent());
verify(signatureService).updateSignatureLabel(eq("user1"), eq("sig123"), eq("new label"));
}
}
@@ -16,8 +16,6 @@ import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.MessageSource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import stirling.software.common.model.ApplicationProperties;
@@ -152,25 +150,6 @@ class UserServiceTest {
assertEquals(AuthenticationType.SAML2, captured.getAuthenticationType());
}
@Test
void getCurrentUsernameReturnsNullWhenAuthenticationMissing() {
SecurityContextHolder.clearContext();
assertNull(userService.getCurrentUsername());
}
@Test
void getCurrentUsernameReturnsUsernameForAuthenticatedPrincipal() {
SecurityContextHolder.getContext()
.setAuthentication(
new UsernamePasswordAuthenticationToken(
"alice", "n/a", java.util.List.of()));
assertEquals("alice", userService.getCurrentUsername());
SecurityContextHolder.clearContext();
}
@Test
void addApiKeyToUserGeneratesAndPersists() {
User user = new User();
+5 -5
View File
@@ -22,14 +22,14 @@ import org.gradle.jvm.toolchain.JavaLanguageVersion
ext {
springBootVersion = "4.0.3"
pdfboxVersion = "3.0.6"
imageioVersion = "3.13.1"
imageioVersion = "3.13.0"
lombokVersion = "1.18.42"
bouncycastleVersion = "1.83"
springSecuritySamlVersion = "7.0.2"
openSamlVersion = "4.3.2"
commonmarkVersion = "0.27.1"
googleJavaFormatVersion = "1.34.1"
logback = "1.5.32"
logback = "1.5.28"
junitPlatformVersion = "1.12.2"
modernJavaVersion = 21
}
@@ -78,7 +78,7 @@ springBoot {
allprojects {
group = 'stirling.software'
version = '2.6.0'
version = '2.5.3'
configurations.configureEach {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
@@ -194,8 +194,8 @@ subprojects {
// - CVE-2022-25647: gson 2.8.9+ (explicit dependency overrides tabula 2.8.7)
// - CVE-2025-66453: rhino 1.7.15 (explicit dependency overrides verapdf 1.7.13)
// Fallback strategy force declarations for additional safety:
resolutionStrategy.force 'com.google.code.gson:gson:2.13.2'
resolutionStrategy.force 'org.mozilla:rhino:1.9.1'
resolutionStrategy.force 'com.google.code.gson:gson:2.8.9'
resolutionStrategy.force 'org.mozilla:rhino:1.7.15'
// CVE-2025-48924: commons-lang3 3.20.0 DoS prevention
resolutionStrategy.force 'org.apache.commons:commons-lang3:3.20.0'
// CVE-2024-47554: commons-io 2.21.0 DoS prevention