From e24e4201424237c074abe148cd85a8d074005c92 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Mon, 14 Apr 2025 10:33:09 +0100 Subject: [PATCH 1/6] Change PR deploy to use security (Enable '/deploypr security' command) (#3345) # Description of Changes Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered 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: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../workflows/PR-Demo-Comment-with-react.yml | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 07c82a158..fb196ca3a 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -37,6 +37,7 @@ jobs: pr_repository: ${{ steps.get-pr-info.outputs.repository }} pr_ref: ${{ steps.get-pr-info.outputs.ref }} comment_id: ${{ github.event.comment.id }} + enable_security: ${{ steps.check-security-flag.outputs.enable_security }} steps: - name: Harden Runner @@ -83,6 +84,19 @@ jobs: core.setOutput('repository', repository); core.setOutput('ref', pr.head.ref); + + - name: Check for security/login flag + id: check-security-flag + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then + echo "Security flags detected in comment" + echo "enable_security=true" >> $GITHUB_OUTPUT + else + echo "No security flags detected in comment" + echo "enable_security=false" >> $GITHUB_OUTPUT + fi - name: Add 'in_progress' reaction to comment id: add-eyes-reaction @@ -140,9 +154,14 @@ jobs: distribution: "temurin" - name: Run Gradle Command - run: ./gradlew clean build + run: | + if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then + export DOCKER_ENABLE_SECURITY=true + else + export DOCKER_ENABLE_SECURITY=false + fi + ./gradlew clean build env: - DOCKER_ENABLE_SECURITY: false STIRLING_PDF_DESKTOP_UI: false - name: Set up Docker Buildx @@ -179,8 +198,19 @@ jobs: - name: Deploy to VPS id: deploy run: | + # Set security settings based on flags + if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then + DOCKER_SECURITY="true" + LOGIN_SECURITY="true" + SECURITY_STATUS="🔒 Security Enabled" + else + DOCKER_SECURITY="false" + LOGIN_SECURITY="false" + SECURITY_STATUS="Security Disabled" + fi + # First create the docker-compose content locally - cat > docker-compose.yml << 'EOF' + cat > docker-compose.yml << EOF version: '3.3' services: stirling-pdf: @@ -193,8 +223,8 @@ jobs: - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw environment: - DOCKER_ENABLE_SECURITY: "false" - SECURITY_ENABLELOGIN: "false" + DOCKER_ENABLE_SECURITY: "${DOCKER_SECURITY}" + SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}" SYSTEM_DEFAULTLOCALE: en-GB UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}" UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest" @@ -208,7 +238,7 @@ jobs: # Then copy the file and execute commands scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml - ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH' + ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH # Create PR-specific directories mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs} @@ -220,6 +250,9 @@ jobs: docker-compose pull docker-compose up -d ENDSSH + + # Set output for use in PR comment + echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV - name: Add success reaction to comment if: success() @@ -270,11 +303,13 @@ jobs: const { GITHUB_REPOSITORY } = process.env; const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const prNumber = ${{ needs.check-comment.outputs.pr_number }}; + const securityStatus = process.env.security_status || "Security Disabled"; const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`; const commentBody = `## 🚀 PR Test Deployment\n\n` + `Your PR has been deployed for testing!\n\n` + - `🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` + + `🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` + + `${securityStatus}\n\n` + `This deployment will be automatically cleaned up when the PR is closed.\n\n`; await github.rest.issues.createComment({ From 1c655f0ba01b7efef00a9dfd3547bfd6c1bf42e1 Mon Sep 17 00:00:00 2001 From: Pedro Fonseca Date: Mon, 14 Apr 2025 10:36:33 +0100 Subject: [PATCH 2/6] Upload File Size Limit (#3334) # Description of Changes The change this PR aims to introduce is a setting for enabling an upload file size limit. The author of the issue mentioned in this PR wanted this feature as they themselves enforced a limit of 50MB file sizes on their NGINX configuration. This was implemented by adding an entry to the [settings.yml.template](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/resources/settings.yml.template) file. This entry has two sub-configurations in which you declare if the application should enable upload file size limiting and then you declare the limit itself. For this to be available in code, a new field in the [System](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java#L280) class was added, one named `uploadLimit`. After that, inside the [AppConfig](url) class, a new thymeleaf bean was created, one called `uploadLimit`. This bean takes the values available in the `System` class and creates a `long` value representing the limit value. This value is interpreted as non-existent if it is `0`, otherwise it is the value in `bytes` of the upload limit. In order to make this value available in the [common.html](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/resources/templates/fragments/common.html) file, where the submitFile form is imported from, a new controller [GlobalUploadLimitWebController](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/java/stirling/software/SPDF/controller/web/GlobalUploadLimitWebController.java) was created. This controller has the tag `ControllerAdvice` so that every controller has the `ModelAttributes` defined within it. I am not sure if this was a good approach but upon first investigations, I couldn't find another method to make these attributes available in every Controller, or template. If there is already a place like this in the code with this specific purpose, please let me know so I can fix it. After making these attributes available, I updated the code in `common.html`to now display the upload limit if it is defined. This was done with localization in mind. Lastly, the [downloader.js](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/resources/static/js/downloader.js) and [fileInput.js](https://github.com/PedroPF1234/Stirling-PDF/blob/e52fc0e478e279169329b7e30782d57b2dbd8cbe/src/main/resources/static/js/fileInput.js) files to include logic to enforce the upload limit if it is defined. The UI updates, when the upload limit is defined, are as so: image When the limit is disabled, the page looks exactly as it did before any implementation: image\\ Thank you. Closes #2903 --- ## Checklist ### General - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [x] 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) - [x] I have performed a self-review of my own code - [x] 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) - [x] 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) - [x] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [x] 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: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> --- .../software/SPDF/config/AppConfig.java | 29 ++++++++++++++++++ .../web/GlobalUploadLimitWebController.java | 30 +++++++++++++++++++ .../SPDF/model/ApplicationProperties.java | 1 + src/main/resources/messages_en_GB.properties | 3 ++ src/main/resources/messages_en_US.properties | 3 ++ src/main/resources/messages_pt_PT.properties | 3 ++ src/main/resources/settings.yml.template | 1 + src/main/resources/static/js/downloader.js | 14 +++++++++ src/main/resources/static/js/fileInput.js | 22 ++++++++++++++ .../resources/templates/fragments/common.html | 9 +++++- 10 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/main/java/stirling/software/SPDF/controller/web/GlobalUploadLimitWebController.java diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index f741a05a4..146f97626 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -20,6 +20,8 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.thymeleaf.spring6.SpringTemplateEngine; +import com.posthog.java.shaded.kotlin.text.Regex; + import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.ApplicationProperties; @@ -107,6 +109,33 @@ public class AppConfig { return (rateLimit != null) ? Boolean.valueOf(rateLimit) : false; } + @Bean(name = "uploadLimit") + public long uploadLimit() { + String maxUploadSize = + applicationProperties.getSystem().getFileUploadLimit() != null + ? applicationProperties.getSystem().getFileUploadLimit() + : ""; + + if (maxUploadSize.isEmpty()) { + return 0; + } else if (!new Regex("^[1-9][0-9]{0,2}[KMGkmg][Bb]$").matches(maxUploadSize)) { + log.error( + "Invalid maxUploadSize format. Expected format: [1-9][0-9]{0,2}[KMGkmg][Bb], but got: {}", + maxUploadSize); + return 0; + } else { + String unit = maxUploadSize.replaceAll("[1-9][0-9]{0,2}", "").toUpperCase(); + String number = maxUploadSize.replaceAll("[KMGkmg][Bb]", ""); + long size = Long.parseLong(number); + return switch (unit) { + case "KB" -> size * 1024; + case "MB" -> size * 1024 * 1024; + case "GB" -> size * 1024 * 1024 * 1024; + default -> 0; + }; + } + } + @Bean(name = "RunningInDocker") public boolean runningInDocker() { return Files.exists(Paths.get("/.dockerenv")); diff --git a/src/main/java/stirling/software/SPDF/controller/web/GlobalUploadLimitWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GlobalUploadLimitWebController.java new file mode 100644 index 000000000..1c8b2b3ac --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/web/GlobalUploadLimitWebController.java @@ -0,0 +1,30 @@ +package stirling.software.SPDF.controller.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +@Component +@ControllerAdvice +public class GlobalUploadLimitWebController { + + @Autowired() private long uploadLimit; + + @ModelAttribute("uploadLimit") + public long populateUploadLimit() { + return uploadLimit; + } + + @ModelAttribute("uploadLimitReadable") + public String populateReadableLimit() { + return humanReadableByteCount(uploadLimit); + } + + private String humanReadableByteCount(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + String pre = "KMGTPE".charAt(exp - 1) + "B"; + return String.format("%.1f %s", bytes / Math.pow(1024, exp), pre); + } +} diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index 6c2960832..d42429619 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -290,6 +290,7 @@ public class ApplicationProperties { private Boolean disableSanitize; private Boolean enableUrlToPDF; private CustomPaths customPaths = new CustomPaths(); + private String fileUploadLimit; public boolean isAnalyticsEnabled() { return this.getEnableAnalytics() != null && this.getEnableAnalytics(); diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 9e8d5bc2a..5b7efbab4 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -10,6 +10,9 @@ multiPdfPrompt=Select PDFs (2+) multiPdfDropPrompt=Select (or drag & drop) all PDFs you require imgPrompt=Select Image(s) genericSubmit=Submit +uploadLimit=Maximum file size: +uploadLimitExceededSingular=is too large. Maximum allowed size is +uploadLimitExceededPlural=are too large. Maximum allowed size is processTimeWarning=Warning: This process can take up to a minute depending on file-size pageOrderPrompt=Custom Page Order (Enter a comma-separated list of page numbers or Functions like 2n+1) : pageSelectionPrompt=Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1) : diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index 69a6cf74f..e2a2eeb75 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -10,6 +10,9 @@ multiPdfPrompt=Select PDFs (2+) multiPdfDropPrompt=Select (or drag & drop) all PDFs you require imgPrompt=Select Image(s) genericSubmit=Submit +uploadLimit=Maximum file size: +uploadLimitExceededSingular=is too large. Maximum allowed size is +uploadLimitExceededPlural=are too large. Maximum allowed size is processTimeWarning=Warning: This process can take up to a minute depending on file-size pageOrderPrompt=Custom Page Order (Enter a comma-separated list of page numbers or Functions like 2n+1) : pageSelectionPrompt=Custom Page Selection (Enter a comma-separated list of page numbers 1,5,6 or Functions like 2n+1) : diff --git a/src/main/resources/messages_pt_PT.properties b/src/main/resources/messages_pt_PT.properties index 842d6e310..88de5abf7 100644 --- a/src/main/resources/messages_pt_PT.properties +++ b/src/main/resources/messages_pt_PT.properties @@ -10,6 +10,9 @@ multiPdfPrompt=Selecione PDFs (2+) multiPdfDropPrompt=Selecione (ou arraste e solte) todos os PDFs necessários imgPrompt=Selecione Imagem(ns) genericSubmit=Submeter +uploadLimit=Tamanho máximo de ficheiro: +uploadLimitExceededSingular=é muito grande. O tamanho máximo permitido é +uploadLimitExceededPlural=são muito grandes. O tamanho máximo permitido é processTimeWarning=Aviso: Este processo pode demorar até um minuto dependendo do tamanho do ficheiro pageOrderPrompt=Ordem Personalizada de Páginas (Insira uma lista de números de página separados por vírgulas ou Funções como 2n+1): pageSelectionPrompt=Seleção Personalizada de Páginas (Insira uma lista de números de página separados por vírgulas 1,5,6 ou Funções como 2n+1): diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 827fec967..1c3ee32ae 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -110,6 +110,7 @@ system: operations: weasyprint: '' #Defaults to /opt/venv/bin/weasyprint unoconvert: '' #Defaults to /opt/venv/bin/unoconvert + fileUploadLimit: '' # Defaults to "". No limit when string is empty. Set a number, between 0 and 999, followed by one of the following strings to set a limit. "KB", "MB", "GB". ui: appName: '' # application's visible name diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js index 900e2539a..3b325b597 100644 --- a/src/main/resources/static/js/downloader.js +++ b/src/main/resources/static/js/downloader.js @@ -43,6 +43,20 @@ firstErrorOccurred = false; const url = this.action; let files = $('#fileInput-input')[0].files; + const uploadLimit = window.stirlingPDF?.uploadLimit ?? 0; + if (uploadLimit > 0) { + const oversizedFiles = Array.from(files).filter(f => f.size > uploadLimit); + if (oversizedFiles.length > 0) { + const names = oversizedFiles.map(f => `"${f.name}"`).join(', '); + if (names.length === 1) { + alert(`${names} ${window.stirlingPDF.uploadLimitExceededSingular} ${window.stirlingPDF.uploadLimitReadable}.`); + } else { + alert(`${names} ${window.stirlingPDF.uploadLimitExceededPlural} ${window.stirlingPDF.uploadLimitReadable}.`); + } + files = Array.from(files).filter(f => f.size <= uploadLimit); + if (files.length === 0) return; + } + } const formData = new FormData(this); const submitButton = document.getElementById('submitBtn'); const showGameBtn = document.getElementById('show-game-btn'); diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js index 32922390b..a67ff1fd6 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -196,6 +196,28 @@ function setupFileInput(chooser) { await checkZipFile(); + const uploadLimit = window.stirlingPDF?.uploadLimit ?? 0; + if (uploadLimit > 0) { + const oversizedFiles = allFiles.filter(f => f.size > uploadLimit); + if (oversizedFiles.length > 0) { + const names = oversizedFiles.map(f => `"${f.name}"`).join(', '); + if (names.length === 1) { + alert(`${names} ${window.stirlingPDF.uploadLimitExceededSingular} ${window.stirlingPDF.uploadLimitReadable}.`); + } else { + alert(`${names} ${window.stirlingPDF.uploadLimitExceededPlural} ${window.stirlingPDF.uploadLimitReadable}.`); + } + allFiles = allFiles.filter(f => f.size <= uploadLimit); + const dataTransfer = new DataTransfer(); + allFiles.forEach(f => dataTransfer.items.add(f)); + input.files = dataTransfer.files; + + if (allFiles.length === 0) { + inputContainer.querySelector('#fileInputText').innerHTML = originalText; + return; + } + } + } + allFiles = await Promise.all( allFiles.map(async (file) => { let decryptedFile = file; diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index dced4be3d..6ba5e710b 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -241,6 +241,10 @@ window.stirlingPDF.sessionExpired = /*[[#{session.expired}]]*/ ''; window.stirlingPDF.refreshPage = /*[[#{session.refreshPage}]]*/ 'Refresh Page'; window.stirlingPDF.error = /*[[#{error}]]*/ "Error"; + window.stirlingPDF.uploadLimit = /*[[${uploadLimit}]]*/ 0; + window.stirlingPDF.uploadLimitReadable = /*[[${uploadLimitReadable}]]*/ 'Unlimited'; + window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is'; + window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is'; })(); @@ -289,8 +293,11 @@
+
+ Maximum file size: + +
-