Security fixes, enterprise stuff and more (#3241)

# Description of Changes

Please provide a summary of the changes, including:

- Enable user to add custom JAVA ops with env JAVA_CUSTOM_OPTS
- Added support for prometheus (enabled via JAVA_CUSTOM_OPTS +
enterprise license)
- Changed settings from enterprise naming to 'Premium'
- KeygenLicense Check to support offline licenses
- Disable URL-to-PDF due to huge security bug
- Remove loud Split PDF logs
- addUsers renamed to adminSettings
- Added Usage analytics page
- Add user button to only be enabled based on total users free
- Improve Merge memory usage


Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
This commit is contained in:
Anthony Stirling
2025-03-25 17:57:17 +00:00
committed by GitHub
parent 86becc61de
commit e151286337
35 changed files with 1603 additions and 267 deletions

View File

@@ -125,8 +125,7 @@ public class MergeController {
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
ByteArrayOutputStream docOutputstream =
new ByteArrayOutputStream(); // Stream for the merged document
File mergedTempFile = null;
PDDocument mergedDocument = null;
boolean removeCertSign = form.isRemoveCertSign();
@@ -139,21 +138,24 @@ public class MergeController {
form.getSortType())); // Sort files based on the given sort type
PDFMergerUtility mergerUtility = new PDFMergerUtility();
long totalSize = 0;
for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize();
File tempFile =
GeneralUtils.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility
}
mergerUtility.setDestinationStream(
docOutputstream); // Set the output stream for the merged document
mergerUtility.mergeDocuments(null); // Merge the documents
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
// Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedPdfBytes);
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
// Remove signatures if removeCertSign is true
if (removeCertSign) {
@@ -180,21 +182,23 @@ public class MergeController {
String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf";
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), mergedFileName); // Return the modified PDF
return WebResponseUtils.boasToWebResponse(
baos, mergedFileName); // Return the modified PDF
} catch (Exception ex) {
log.error("Error in merge pdf process", ex);
throw ex;
} finally {
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
for (File file : filesToDelete) {
if (file != null) {
Files.deleteIfExists(file.toPath()); // Delete temporary files
}
}
docOutputstream.close();
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
if (mergedTempFile != null) {
Files.deleteIfExists(mergedTempFile.toPath());
}
}
}

View File

@@ -40,9 +40,6 @@ public class SplitPdfBySizeController {
@Autowired
public SplitPdfBySizeController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
log.info(
"SplitPdfBySizeController initialized with pdfDocumentFactory: {}",
pdfDocumentFactory.getClass().getSimpleName());
}
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@@ -57,53 +54,49 @@ public class SplitPdfBySizeController {
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
log.info("Starting PDF split process with request: {}", request);
log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput();
log.info(
"File received: name={}, size={} bytes",
file.getOriginalFilename(),
file.getSize());
Path zipFile = Files.createTempFile("split_documents", ".zip");
log.info("Created temporary zip file: {}", zipFile);
log.debug("Created temporary zip file: {}", zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
log.info("Base filename for output: {}", filename);
log.debug("Base filename for output: {}", filename);
byte[] data = null;
try {
log.info("Reading input file bytes");
log.debug("Reading input file bytes");
byte[] pdfBytes = file.getBytes();
log.info("Successfully read {} bytes from input file", pdfBytes.length);
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
log.info("Creating ZIP output stream");
log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
log.info("Loading PDF document");
log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.info(
log.debug(
"Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages());
int type = request.getSplitType();
String value = request.getSplitValue();
log.info("Split type: {}, Split value: {}", type, value);
log.debug("Split type: {}, Split value: {}", type, value);
if (type == 0) {
log.info("Processing split by size");
log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.info("Max bytes per document: {}", maxBytes);
log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
log.info("Processing split by page count");
log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value);
log.info("Pages per document: {}", pageCount);
log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) {
log.info("Processing split by document count");
log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value);
log.info("Total number of documents: {}", documentCount);
log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
@@ -111,7 +104,7 @@ public class SplitPdfBySizeController {
"Invalid argument for split type: " + type);
}
log.info("PDF splitting completed successfully");
log.debug("PDF splitting completed successfully");
} catch (Exception e) {
log.error("Error loading or processing PDF document", e);
throw e;
@@ -126,23 +119,23 @@ public class SplitPdfBySizeController {
throw e; // Re-throw to ensure proper error response
} finally {
try {
log.info("Reading ZIP file data");
log.debug("Reading ZIP file data");
data = Files.readAllBytes(zipFile);
log.info("Successfully read {} bytes from ZIP file", data.length);
log.debug("Successfully read {} bytes from ZIP file", data.length);
} catch (IOException e) {
log.error("Error reading ZIP file data", e);
}
try {
log.info("Deleting temporary ZIP file");
log.debug("Deleting temporary ZIP file");
boolean deleted = Files.deleteIfExists(zipFile);
log.info("Temporary ZIP file deleted: {}", deleted);
log.debug("Temporary ZIP file deleted: {}", deleted);
} catch (IOException e) {
log.error("Error deleting temporary ZIP file", e);
}
}
log.info("Returning response with {} bytes of data", data != null ? data.length : 0);
log.debug("Returning response with {} bytes of data", data != null ? data.length : 0);
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
}
@@ -150,7 +143,7 @@ public class SplitPdfBySizeController {
private void handleSplitBySize(
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.info("Starting handleSplitBySize with maxBytes={}", maxBytes);
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
@@ -163,7 +156,7 @@ public class SplitPdfBySizeController {
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
PDPage page = sourceDocument.getPage(pageIndex);
log.info("Processing page {} of {}", pageIndex + 1, totalPages);
log.debug("Processing page {} of {}", pageIndex + 1, totalPages);
// Add the page to current document
PDPage newPage = new PDPage(page.getCOSObject());
@@ -177,21 +170,21 @@ public class SplitPdfBySizeController {
|| (pageAdded >= 20); // Always check after 20 pages
if (shouldCheckSize) {
log.info("Performing size check after {} pages", pageAdded);
log.debug("Performing size check after {} pages", pageAdded);
ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream();
currentDoc.save(checkSizeStream);
long actualSize = checkSizeStream.size();
log.info("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
log.debug("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
if (actualSize > maxBytes) {
// We exceeded the limit - remove the last page and save
if (currentDoc.getNumberOfPages() > 1) {
currentDoc.removePage(currentDoc.getNumberOfPages() - 1);
pageIndex--; // Process this page again in the next document
log.info("Size limit exceeded - removed last page");
log.debug("Size limit exceeded - removed last page");
}
log.info(
log.debug(
"Saving document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
@@ -206,7 +199,7 @@ public class SplitPdfBySizeController {
int pagesToLookAhead = Math.min(5, totalPages - pageIndex - 1);
if (pagesToLookAhead > 0) {
log.info(
log.debug(
"Testing {} upcoming pages for potential addition",
pagesToLookAhead);
@@ -231,12 +224,12 @@ public class SplitPdfBySizeController {
if (testSize <= maxBytes) {
extraPagesAdded++;
log.info(
log.debug(
"Test: Can add page {} (size would be {})",
testPageIndex + 1,
testSize);
} else {
log.info(
log.debug(
"Test: Cannot add page {} (size would be {})",
testPageIndex + 1,
testSize);
@@ -248,7 +241,7 @@ public class SplitPdfBySizeController {
// Add the pages we verified would fit
if (extraPagesAdded > 0) {
log.info("Adding {} verified pages ahead", extraPagesAdded);
log.debug("Adding {} verified pages ahead", extraPagesAdded);
for (int i = 0; i < extraPagesAdded; i++) {
int extraPageIndex = pageIndex + 1 + i;
PDPage extraPage = sourceDocument.getPage(extraPageIndex);
@@ -265,26 +258,26 @@ public class SplitPdfBySizeController {
// Save final document if it has any pages
if (currentDoc.getNumberOfPages() > 0) {
log.info(
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
}
log.info("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
}
private void handleSplitByPageCount(
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.info("Starting handleSplitByPageCount with pageCount={}", pageCount);
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
int currentPageCount = 0;
log.info("Creating initial output document");
log.debug("Creating initial output document");
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.info("Successfully created initial output document");
log.debug("Successfully created initial output document");
} catch (Exception e) {
log.error("Error creating initial output document", e);
throw new IOException("Failed to create initial output document", e);
@@ -293,49 +286,49 @@ public class SplitPdfBySizeController {
int fileIndex = 1;
int pageIndex = 0;
int totalPages = sourceDocument.getNumberOfPages();
log.info("Processing {} pages", totalPages);
log.debug("Processing {} pages", totalPages);
try {
for (PDPage page : sourceDocument.getPages()) {
pageIndex++;
log.info("Processing page {} of {}", pageIndex, totalPages);
log.debug("Processing page {} of {}", pageIndex, totalPages);
try {
log.info("Adding page {} to current document", pageIndex);
log.debug("Adding page {} to current document", pageIndex);
currentDoc.addPage(page);
log.info("Successfully added page {} to current document", pageIndex);
log.debug("Successfully added page {} to current document", pageIndex);
} catch (Exception e) {
log.error("Error adding page {} to current document", pageIndex, e);
throw new IOException("Failed to add page to document", e);
}
currentPageCount++;
log.info("Current page count: {}/{}", currentPageCount, pageCount);
log.debug("Current page count: {}/{}", currentPageCount, pageCount);
if (currentPageCount == pageCount) {
log.info(
log.debug(
"Reached target page count ({}), saving current document as part {}",
pageCount,
fileIndex);
try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved document part {}", fileIndex - 1);
log.debug("Successfully saved document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving document part {}", fileIndex - 1, e);
throw e;
}
try {
log.info("Creating new document for next part");
log.debug("Creating new document for next part");
currentDoc = new PDDocument();
log.info("Successfully created new document");
log.debug("Successfully created new document");
} catch (Exception e) {
log.error("Error creating new document for next part", e);
throw new IOException("Failed to create new document", e);
}
currentPageCount = 0;
log.info("Reset current page count to 0");
log.debug("Reset current page count to 0");
}
}
} catch (Exception e) {
@@ -346,34 +339,34 @@ public class SplitPdfBySizeController {
// Add the last document if it contains any pages
try {
if (currentDoc.getPages().getCount() != 0) {
log.info(
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getPages().getCount(),
fileIndex);
try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved final document part {}", fileIndex - 1);
log.debug("Successfully saved final document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving final document part {}", fileIndex - 1, e);
throw e;
}
} else {
log.info("Final document has no pages, skipping");
log.debug("Final document has no pages, skipping");
}
} catch (Exception e) {
log.error("Error checking or saving final document", e);
throw new IOException("Failed to process final document", e);
} finally {
try {
log.info("Closing final document");
log.debug("Closing final document");
currentDoc.close();
log.info("Successfully closed final document");
log.debug("Successfully closed final document");
} catch (Exception e) {
log.error("Error closing final document", e);
}
}
log.info("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
log.debug("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
}
private void handleSplitByDocCount(
@@ -382,40 +375,40 @@ public class SplitPdfBySizeController {
ZipOutputStream zipOut,
String baseFilename)
throws IOException {
log.info("Starting handleSplitByDocCount with documentCount={}", documentCount);
log.debug("Starting handleSplitByDocCount with documentCount={}", documentCount);
int totalPageCount = sourceDocument.getNumberOfPages();
log.info("Total pages in source document: {}", totalPageCount);
log.debug("Total pages in source document: {}", totalPageCount);
int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount;
log.info("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
log.debug("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
int currentPageIndex = 0;
int fileIndex = 1;
for (int i = 0; i < documentCount; i++) {
log.info("Creating document {} of {}", i + 1, documentCount);
log.debug("Creating document {} of {}", i + 1, documentCount);
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.info("Successfully created document {} of {}", i + 1, documentCount);
log.debug("Successfully created document {} of {}", i + 1, documentCount);
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw new IOException("Failed to create document", e);
}
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
log.info("Adding {} pages to document {}", pagesToAdd, i + 1);
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
for (int j = 0; j < pagesToAdd; j++) {
try {
log.info(
log.debug(
"Adding page {} (index {}) to document {}",
j + 1,
currentPageIndex,
i + 1);
currentDoc.addPage(sourceDocument.getPage(currentPageIndex));
log.info("Successfully added page {} to document {}", j + 1, i + 1);
log.debug("Successfully added page {} to document {}", j + 1, i + 1);
currentPageIndex++;
} catch (Exception e) {
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
@@ -424,37 +417,37 @@ public class SplitPdfBySizeController {
}
try {
log.info("Saving document {} with {} pages", i + 1, pagesToAdd);
log.debug("Saving document {} with {} pages", i + 1, pagesToAdd);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.info("Successfully saved document {}", i + 1);
log.debug("Successfully saved document {}", i + 1);
} catch (Exception e) {
log.error("Error saving document {}", i + 1, e);
throw e;
}
}
log.info("Completed handleSplitByDocCount with {} documents created", documentCount);
log.debug("Completed handleSplitByDocCount with {} documents created", documentCount);
}
private void saveDocumentToZip(
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
throws IOException {
log.info("Starting saveDocumentToZip for document part {}", index);
log.debug("Starting saveDocumentToZip for document part {}", index);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
log.info("Saving document part {} to byte array", index);
log.debug("Saving document part {} to byte array", index);
document.save(outStream);
log.info("Successfully saved document part {} ({} bytes)", index, outStream.size());
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
} catch (Exception e) {
log.error("Error saving document part {} to byte array", index, e);
throw new IOException("Failed to save document to byte array", e);
}
try {
log.info("Closing document part {}", index);
log.debug("Closing document part {}", index);
document.close();
log.info("Successfully closed document part {}", index);
log.debug("Successfully closed document part {}", index);
} catch (Exception e) {
log.error("Error closing document part {}", index, e);
// Continue despite close error
@@ -463,17 +456,17 @@ public class SplitPdfBySizeController {
try {
// Create a new zip entry
String entryName = baseFilename + "_" + index + ".pdf";
log.info("Creating ZIP entry: {}", entryName);
log.debug("Creating ZIP entry: {}", entryName);
ZipEntry zipEntry = new ZipEntry(entryName);
zipOut.putNextEntry(zipEntry);
byte[] bytes = outStream.toByteArray();
log.info("Writing {} bytes to ZIP entry", bytes.length);
log.debug("Writing {} bytes to ZIP entry", bytes.length);
zipOut.write(bytes);
log.info("Closing ZIP entry");
log.debug("Closing ZIP entry");
zipOut.closeEntry();
log.info("Successfully added document part {} to ZIP", index);
log.debug("Successfully added document part {} to ZIP", index);
} catch (Exception e) {
log.error("Error adding document part {} to ZIP", index, e);
throw new IOException("Failed to add document to ZIP file", e);

View File

@@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
@@ -47,10 +48,15 @@ public class UserController {
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
private final UserService userService;
private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
public UserController(
UserService userService,
SessionPersistentRegistry sessionRegistry,
ApplicationProperties applicationProperties) {
this.userService = userService;
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties;
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@@ -194,39 +200,44 @@ public class UserController {
boolean forceChange)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) {
return new RedirectView("/addUsers?messageType=invalidUsername", true);
return new RedirectView("/adminSettings?messageType=invalidUsername", true);
}
if (applicationProperties.getPremium().isEnabled()
&& applicationProperties.getPremium().getMaxUsers()
<= userService.getTotalUsersCount()) {
return new RedirectView("/adminSettings?messageType=maxUsersReached", true);
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true);
return new RedirectView("/adminSettings?messageType=usernameExists", true);
}
}
if (userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=usernameExists", true);
return new RedirectView("/adminSettings?messageType=usernameExists", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role);
} else {
if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true);
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange);
}
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@@ -239,32 +250,32 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
return new RedirectView("/adminSettings?messageType=downgradeCurrentUser", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole", true);
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
User user = userOpt.get();
userService.changeRole(user, role);
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@@ -277,16 +288,16 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=userNotFound", true);
return new RedirectView("/adminSettings?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
return new RedirectView("/adminSettings?messageType=disabledCurrentUser", true);
}
User user = userOpt.get();
userService.changeUserEnabled(user, enabled);
@@ -314,7 +325,7 @@ public class UserController {
}
}
return new RedirectView(
"/addUsers", // Redirect to account page after adding the user
"/adminSettings", // Redirect to account page after adding the user
true);
}
@@ -323,13 +334,13 @@ public class UserController {
public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
return new RedirectView("/adminSettings?messageType=deleteUsernameExists", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
return new RedirectView("/adminSettings?messageType=deleteCurrentUser", true);
}
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
@@ -339,7 +350,7 @@ public class UserController {
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
}
userService.deleteUser(username);
return new RedirectView("/addUsers", true);
return new RedirectView("/adminSettings", true);
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")

View File

@@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
@@ -35,12 +36,16 @@ public class ConvertWebsiteToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties;
@Autowired
public ConvertWebsiteToPDF(
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
CustomPDFDocumentFactory pdfDocumentFactory,
RuntimePathConfig runtimePathConfig,
ApplicationProperties applicationProperties) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig;
this.applicationProperties = applicationProperties;
}
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@@ -53,6 +58,9 @@ public class ConvertWebsiteToPDF {
throws IOException, InterruptedException {
String URL = request.getUrlInput();
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
}
// Validate the URL format
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided.");

View File

@@ -118,11 +118,11 @@ public class AccountWebController {
if (securityProps.isSaml2Active()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()
&& applicationProperties.getEnterpriseEdition().isEnabled()) {
&& applicationProperties.getPremium().isEnabled()) {
String samlIdp = saml2.getProvider();
String saml2AuthenticationPath = "/saml2/authenticate/" + saml2.getRegistrationId();
if (applicationProperties.getEnterpriseEdition().isSsoAutoLogin()) {
if (applicationProperties.getPremium().getProFeatures().isSsoAutoLogin()) {
return "redirect:" + request.getRequestURL() + saml2AuthenticationPath;
} else {
providerList.put(saml2AuthenticationPath, samlIdp + " (SAML 2)");
@@ -195,7 +195,13 @@ public class AccountWebController {
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers")
@GetMapping("/usage")
public String showUsage() {
return "usage";
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/adminSettings")
public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll();
@@ -318,7 +324,9 @@ public class AccountWebController {
model.addAttribute("totalUsers", allUsers.size());
model.addAttribute("activeUsers", activeUsers);
model.addAttribute("disabledUsers", disabledUsers);
return "addUsers";
model.addAttribute("maxEnterpriseUsers", applicationProperties.getPremium().getMaxUsers());
return "adminSettings";
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")

View File

@@ -22,6 +22,7 @@ import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointInspector;
import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties;
@@ -32,15 +33,17 @@ import stirling.software.SPDF.model.ApplicationProperties;
public class MetricsController {
private final ApplicationProperties applicationProperties;
private final MeterRegistry meterRegistry;
private final EndpointInspector endpointInspector;
private boolean metricsEnabled;
public MetricsController(
ApplicationProperties applicationProperties, MeterRegistry meterRegistry) {
ApplicationProperties applicationProperties,
MeterRegistry meterRegistry,
EndpointInspector endpointInspector) {
this.applicationProperties = applicationProperties;
this.meterRegistry = meterRegistry;
this.endpointInspector = endpointInspector;
}
@PostConstruct
@@ -208,25 +211,43 @@ public class MetricsController {
}
private double getRequestCount(String method, Optional<String> endpoint) {
log.info(
"Getting request count for method: {}, endpoint: {}",
method,
endpoint.orElse("all"));
double count =
meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter ->
!endpoint.isPresent()
|| endpoint.get()
.equals(counter.getId().getTag("uri")))
.mapToDouble(Counter::count)
.sum();
log.info("Request count: {}", count);
return count;
return meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter -> {
String uri = counter.getId().getTag("uri");
// Apply filtering logic - Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
// Filter for specific endpoint if provided
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.mapToDouble(Counter::count)
.sum();
}
private List<EndpointCount> getEndpointCounts(String method) {
log.info("Getting endpoint counts for method: {}", method);
Map<String, Double> counts = new HashMap<>();
meterRegistry
.find("http.requests")
@@ -235,28 +256,72 @@ public class MetricsController {
.forEach(
counter -> {
String uri = counter.getId().getTag("uri");
// Skip if uri is null
if (uri == null) {
return;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return;
}
if (uri.contains(".txt")) {
return;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return;
}
counts.merge(uri, counter.count(), Double::sum);
});
List<EndpointCount> result =
counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
log.info("Found {} endpoints with counts", result.size());
return result;
return counts.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
}
private double getUniqueUserCount(String method, Optional<String> endpoint) {
log.info(
"Getting unique user count for method: {}, endpoint: {}",
method,
endpoint.orElse("all"));
Set<String> uniqueUsers = new HashSet<>();
meterRegistry.find("http.requests").tag("method", method).counters().stream()
.filter(
counter ->
!endpoint.isPresent()
|| endpoint.get().equals(counter.getId().getTag("uri")))
counter -> {
String uri = counter.getId().getTag("uri");
// Skip if uri is null
if (uri == null) {
return false;
}
// For POST requests, only include if they start with /api/v1
if ("POST".equals(method) && !uri.contains("api/v1")) {
return false;
}
if (uri.contains(".txt")) {
return false;
}
// For GET requests, validate if we have a list of valid endpoints
final boolean validateGetEndpoints =
endpointInspector.getValidGetEndpoints().size() != 0;
if ("GET".equals(method)
&& validateGetEndpoints
&& !endpointInspector.isValidGetEndpoint(uri)) {
log.debug("Skipping invalid GET endpoint: {}", uri);
return false;
}
return !endpoint.isPresent() || endpoint.get().equals(uri);
})
.forEach(
counter -> {
String session = counter.getId().getTag("session");
@@ -264,12 +329,10 @@ public class MetricsController {
uniqueUsers.add(session);
}
});
log.info("Unique user count: {}", uniqueUsers.size());
return uniqueUsers.size();
}
private List<EndpointCount> getUniqueUserCounts(String method) {
log.info("Getting unique user counts for method: {}", method);
Map<String, Set<String>> uniqueUsers = new HashMap<>();
meterRegistry
.find("http.requests")
@@ -283,13 +346,10 @@ public class MetricsController {
uniqueUsers.computeIfAbsent(uri, k -> new HashSet<>()).add(session);
}
});
List<EndpointCount> result =
uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
log.info("Found {} endpoints with unique user counts", result.size());
return result;
return uniqueUsers.entrySet().stream()
.map(entry -> new EndpointCount(entry.getKey(), entry.getValue().size()))
.sorted(Comparator.comparing(EndpointCount::getCount).reversed())
.collect(Collectors.toList());
}
@GetMapping("/uptime")