# Description of Changes This pull request introduces several improvements to enhance error handling, internationalization, and documentation in the codebase. The key changes include the addition of `ExceptionUtils` and `I18nUtils` utility classes for consistent exception handling and internationalized messages, updates to documentation paths, and modifications to existing methods to integrate the new utilities. ### Error Handling Enhancements: * **Added `ExceptionUtils` utility class**: Provides standardized methods for creating and handling exceptions with internationalized error messages, including specific handling for PDF corruption, encryption issues, and other file-related errors. * **Integrated `ExceptionUtils` into `CustomPDFDocumentFactory`**: Updated `loadFromFile` and `loadFromBytes` methods to log and handle exceptions using `ExceptionUtils`, ensuring consistent error handling across PDF operations. [[1]](diffhunk://#diff-10208c1fc2e04631a8cf2a2a99b2a1160e532e75a7b840ad752f3b0130b89851R358-R363) [[2]](diffhunk://#diff-10208c1fc2e04631a8cf2a2a99b2a1160e532e75a7b840ad752f3b0130b89851R375-R381) * **Updated `FileToPdf` to use `ExceptionUtils`**: Replaced direct exception throwing with `ExceptionUtils.createHtmlFileRequiredException` for unsupported file formats. ### Internationalization Improvements: * **Added `I18nUtils` utility class**: Centralized access to Spring's `MessageSource` for retrieving localized messages, enabling consistent internationalization across the application. ### Documentation Updates: * **Updated documentation paths in `CONTRIBUTING.md` and `README.md`**: Changed paths to reference the new `devGuide` folder for developer documentation and translation guides. [[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L28-R28) [[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L40-R51) [[3]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L171-L174) --- ## 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>
214 lines
9.0 KiB
Java
214 lines
9.0 KiB
Java
package stirling.software.common.util;
|
|
|
|
import java.io.*;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.file.FileVisitResult;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.SimpleFileVisitor;
|
|
import java.nio.file.attribute.BasicFileAttributes;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.stream.Stream;
|
|
import java.util.zip.ZipEntry;
|
|
import java.util.zip.ZipInputStream;
|
|
import java.util.zip.ZipOutputStream;
|
|
|
|
import io.github.pixee.security.ZipSecurity;
|
|
|
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
|
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
|
|
|
public class FileToPdf {
|
|
|
|
public static byte[] convertHtmlToPdf(
|
|
String weasyprintPath,
|
|
HTMLToPdfRequest request,
|
|
byte[] fileBytes,
|
|
String fileName,
|
|
boolean disableSanitize,
|
|
TempFileManager tempFileManager)
|
|
throws IOException, InterruptedException {
|
|
|
|
try (TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
|
|
try (TempFile tempInputFile =
|
|
new TempFile(
|
|
tempFileManager,
|
|
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
|
|
|
|
if (fileName.toLowerCase().endsWith(".html")) {
|
|
String sanitizedHtml =
|
|
sanitizeHtmlContent(
|
|
new String(fileBytes, StandardCharsets.UTF_8), disableSanitize);
|
|
Files.write(
|
|
tempInputFile.getPath(),
|
|
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
|
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
|
Files.write(tempInputFile.getPath(), fileBytes);
|
|
sanitizeHtmlFilesInZip(
|
|
tempInputFile.getPath(), disableSanitize, tempFileManager);
|
|
} else {
|
|
throw ExceptionUtils.createHtmlFileRequiredException();
|
|
}
|
|
|
|
List<String> command = new ArrayList<>();
|
|
command.add(weasyprintPath);
|
|
command.add("-e");
|
|
command.add("utf-8");
|
|
command.add("-v");
|
|
command.add("--pdf-forms");
|
|
command.add(tempInputFile.getAbsolutePath());
|
|
command.add(tempOutputFile.getAbsolutePath());
|
|
|
|
ProcessExecutorResult returnCode =
|
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
|
|
.runCommandWithOutputHandling(command);
|
|
|
|
byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
|
|
try {
|
|
return pdfBytes;
|
|
} catch (Exception e) {
|
|
pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
|
|
if (pdfBytes.length < 1) {
|
|
throw e;
|
|
}
|
|
return pdfBytes;
|
|
}
|
|
} // tempInputFile auto-closed
|
|
} // tempOutputFile auto-closed
|
|
}
|
|
|
|
private static String sanitizeHtmlContent(String htmlContent, boolean disableSanitize) {
|
|
return (!disableSanitize) ? CustomHtmlSanitizer.sanitize(htmlContent) : htmlContent;
|
|
}
|
|
|
|
private static void sanitizeHtmlFilesInZip(
|
|
Path zipFilePath, boolean disableSanitize, TempFileManager tempFileManager)
|
|
throws IOException {
|
|
try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
|
|
try (ZipInputStream zipIn =
|
|
ZipSecurity.createHardenedInputStream(
|
|
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
|
ZipEntry entry = zipIn.getNextEntry();
|
|
while (entry != null) {
|
|
Path filePath =
|
|
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
|
|
if (!entry.isDirectory()) {
|
|
Files.createDirectories(filePath.getParent());
|
|
if (entry.getName().toLowerCase().endsWith(".html")
|
|
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
|
String content =
|
|
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
|
String sanitizedContent = sanitizeHtmlContent(content, disableSanitize);
|
|
Files.write(
|
|
filePath, sanitizedContent.getBytes(StandardCharsets.UTF_8));
|
|
} else {
|
|
Files.copy(zipIn, filePath);
|
|
}
|
|
}
|
|
zipIn.closeEntry();
|
|
entry = zipIn.getNextEntry();
|
|
}
|
|
}
|
|
|
|
// Repack the sanitized files
|
|
zipDirectory(tempUnzippedDir.getPath(), zipFilePath);
|
|
} // tempUnzippedDir auto-cleaned
|
|
}
|
|
|
|
private static void zipDirectory(Path sourceDir, Path zipFilePath) throws IOException {
|
|
try (ZipOutputStream zos =
|
|
new ZipOutputStream(new FileOutputStream(zipFilePath.toFile()))) {
|
|
Files.walk(sourceDir)
|
|
.filter(path -> !Files.isDirectory(path))
|
|
.forEach(
|
|
path -> {
|
|
ZipEntry zipEntry =
|
|
new ZipEntry(sourceDir.relativize(path).toString());
|
|
try {
|
|
zos.putNextEntry(zipEntry);
|
|
Files.copy(path, zos);
|
|
zos.closeEntry();
|
|
} catch (IOException e) {
|
|
throw new UncheckedIOException(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private static void deleteDirectory(Path dir) throws IOException {
|
|
Files.walkFileTree(
|
|
dir,
|
|
new SimpleFileVisitor<Path>() {
|
|
@Override
|
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
|
throws IOException {
|
|
Files.delete(file);
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
|
|
@Override
|
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
|
throws IOException {
|
|
Files.delete(dir);
|
|
return FileVisitResult.CONTINUE;
|
|
}
|
|
});
|
|
}
|
|
|
|
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
|
|
Path tempDirectory = Files.createTempDirectory("unzipped_");
|
|
try (ZipInputStream zipIn =
|
|
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
|
ZipEntry entry = zipIn.getNextEntry();
|
|
while (entry != null) {
|
|
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
|
if (entry.isDirectory()) {
|
|
Files.createDirectories(filePath); // Explicitly create the directory structure
|
|
} else {
|
|
Files.createDirectories(
|
|
filePath.getParent()); // Create parent directories if they don't exist
|
|
Files.copy(zipIn, filePath);
|
|
}
|
|
zipIn.closeEntry();
|
|
entry = zipIn.getNextEntry();
|
|
}
|
|
}
|
|
|
|
// Search for the main HTML file.
|
|
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
|
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
|
|
|
|
if (htmlFiles.isEmpty()) {
|
|
throw new IOException("No HTML files found in the unzipped directory.");
|
|
}
|
|
|
|
// Prioritize 'index.html' if it exists, otherwise use the first .html file
|
|
for (Path htmlFile : htmlFiles) {
|
|
if ("index.html".equals(htmlFile.getFileName().toString())) {
|
|
return htmlFile;
|
|
}
|
|
}
|
|
|
|
return htmlFiles.get(0);
|
|
}
|
|
}
|
|
|
|
static String sanitizeZipFilename(String entryName) {
|
|
if (entryName == null || entryName.trim().isEmpty()) {
|
|
return "";
|
|
}
|
|
// Remove any drive letters (e.g., "C:\") and leading forward/backslashes
|
|
entryName = entryName.replaceAll("^[a-zA-Z]:[\\\\/]+", "");
|
|
entryName = entryName.replaceAll("^[\\\\/]+", "");
|
|
|
|
// Recursively remove path traversal sequences
|
|
while (entryName.contains("../") || entryName.contains("..\\")) {
|
|
entryName = entryName.replace("../", "").replace("..\\", "");
|
|
}
|
|
// Normalize all backslashes to forward slashes
|
|
entryName = entryName.replaceAll("\\\\", "/");
|
|
return entryName;
|
|
}
|
|
}
|