From aec5a8ddc5a66d446105d21cb33bbba64a983e39 Mon Sep 17 00:00:00 2001 From: Ludy Date: Thu, 7 Aug 2025 14:57:47 +0200 Subject: [PATCH 01/23] feat(common): add configurable maxDPI limit for PDF-to-image conversion (#4129) # Description of Changes - **What was changed:** Added a new `maxDPI` property under `system` in `ApplicationProperties`; updated `PdfUtils` to retrieve and enforce this configurable limit instead of a hard-coded constant; modified `ConverterWebController` and the PDF-to-image template to expose the limit to users; added `pdfToImage.dpi` entries across all translation files; updated `settings.yml.template` and `DeveloperGuide.md` to document the new setting. - **Why the change was made:** To allow deployments to tune the maximum DPI for PDF-to-image conversions based on available resources, preventing excessive memory usage and crashes caused by arbitrarily high DPI values. Closes #3985 --- ## 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/devGuide/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/devGuide/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) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../common/model/ApplicationProperties.java | 1 + .../stirling/software/common/util/PdfUtils.java | 12 +++++++++--- .../controller/web/ConverterWebController.java | 9 +++++++++ .../src/main/resources/messages_ar_AR.properties | 1 + .../src/main/resources/messages_az_AZ.properties | 1 + .../src/main/resources/messages_bg_BG.properties | 1 + .../src/main/resources/messages_bo_CN.properties | 1 + .../src/main/resources/messages_ca_CA.properties | 1 + .../src/main/resources/messages_cs_CZ.properties | 1 + .../src/main/resources/messages_da_DK.properties | 1 + .../src/main/resources/messages_de_DE.properties | 1 + .../src/main/resources/messages_el_GR.properties | 1 + .../src/main/resources/messages_en_GB.properties | 1 + .../src/main/resources/messages_en_US.properties | 1 + .../src/main/resources/messages_es_ES.properties | 1 + .../src/main/resources/messages_eu_ES.properties | 1 + .../src/main/resources/messages_fa_IR.properties | 1 + .../src/main/resources/messages_fr_FR.properties | 1 + .../src/main/resources/messages_ga_IE.properties | 1 + .../src/main/resources/messages_hi_IN.properties | 1 + .../src/main/resources/messages_hr_HR.properties | 1 + .../src/main/resources/messages_hu_HU.properties | 1 + .../src/main/resources/messages_id_ID.properties | 1 + .../src/main/resources/messages_it_IT.properties | 1 + .../src/main/resources/messages_ja_JP.properties | 1 + .../src/main/resources/messages_ko_KR.properties | 1 + .../src/main/resources/messages_ml_IN.properties | 1 + .../src/main/resources/messages_nl_NL.properties | 1 + .../src/main/resources/messages_no_NB.properties | 1 + .../src/main/resources/messages_pl_PL.properties | 1 + .../src/main/resources/messages_pt_BR.properties | 1 + .../src/main/resources/messages_pt_PT.properties | 1 + .../src/main/resources/messages_ro_RO.properties | 1 + .../src/main/resources/messages_ru_RU.properties | 1 + .../src/main/resources/messages_sk_SK.properties | 1 + .../src/main/resources/messages_sl_SI.properties | 1 + .../main/resources/messages_sr_LATN_RS.properties | 1 + .../src/main/resources/messages_sv_SE.properties | 1 + .../src/main/resources/messages_th_TH.properties | 1 + .../src/main/resources/messages_tr_TR.properties | 1 + .../src/main/resources/messages_uk_UA.properties | 1 + .../src/main/resources/messages_vi_VN.properties | 1 + .../src/main/resources/messages_zh_CN.properties | 1 + .../src/main/resources/messages_zh_TW.properties | 1 + app/core/src/main/resources/settings.yml.template | 1 + .../resources/templates/convert/pdf-to-img.html | 15 +++++++++++++++ devGuide/DeveloperGuide.md | 1 + 47 files changed, 77 insertions(+), 3 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 802a55831..fb93ef345 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -311,6 +311,7 @@ public class ApplicationProperties { private Boolean enableAnalytics; private Datasource datasource; private Boolean disableSanitize; + private int maxDPI; private Boolean enableUrlToPDF; private Html html = new Html(); private CustomPaths customPaths = new CustomPaths(); diff --git a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java index ec269e47d..6f4305bd3 100644 --- a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java @@ -35,6 +35,7 @@ import io.github.pixee.security.Filenames; import lombok.extern.slf4j.Slf4j; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; @Slf4j @@ -145,13 +146,18 @@ public class PdfUtils { throws IOException, Exception { // Validate and limit DPI to prevent excessive memory usage - final int MAX_SAFE_DPI = 500; // Maximum safe DPI to prevent memory issues - if (DPI > MAX_SAFE_DPI) { + int maxSafeDpi = 500; // Default maximum safe DPI + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + maxSafeDpi = properties.getSystem().getMaxDPI(); + } + if (DPI > maxSafeDpi) { throw ExceptionUtils.createIllegalArgumentException( "error.dpiExceedsLimit", "DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause memory issues and crashes. Please use a lower DPI value.", DPI, - MAX_SAFE_DPI); + maxSafeDpi); } try (PDDocument document = pdfDocumentFactory.load(inputStream)) { diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 34f8a8daa..1c05aaabd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -8,6 +8,8 @@ import org.springframework.web.servlet.ModelAndView; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.tags.Tag; +import stirling.software.common.model.ApplicationProperties; +import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; @Controller @@ -62,6 +64,13 @@ public class ConverterWebController { @Hidden public String pdfToimgForm(Model model) { boolean isPython = CheckProgramInstall.isPythonAvailable(); + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + model.addAttribute("maxDPI", properties.getSystem().getMaxDPI()); + } else { + model.addAttribute("maxDPI", 500); // Default value if not set + } model.addAttribute("isPython", isPython); model.addAttribute("currentPage", "pdf-to-img"); return "convert/pdf-to-img"; diff --git a/app/core/src/main/resources/messages_ar_AR.properties b/app/core/src/main/resources/messages_ar_AR.properties index 2993800ac..841bdd285 100644 --- a/app/core/src/main/resources/messages_ar_AR.properties +++ b/app/core/src/main/resources/messages_ar_AR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=نوع اللون pdfToImage.color=اللون pdfToImage.grey=تدرج الرمادي pdfToImage.blackwhite=أبيض وأسود (قد يفقد البيانات!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=تحويل pdfToImage.info=Python غير مثبت. مطلوب لتحويل WebP. pdfToImage.placeholder=(مثال: 1,2,8 أو 4,7,12-16 أو 2n-1) diff --git a/app/core/src/main/resources/messages_az_AZ.properties b/app/core/src/main/resources/messages_az_AZ.properties index c719d7139..5da8d541b 100644 --- a/app/core/src/main/resources/messages_az_AZ.properties +++ b/app/core/src/main/resources/messages_az_AZ.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Rəng Tipi pdfToImage.color=Rəng pdfToImage.grey=Boz Tonlama pdfToImage.blackwhite=Qara və Ağ (Data İtə Bilər) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Çevir pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir pdfToImage.placeholder=(məsələn, 1,2,8 və ya 4,7,12-16 və ya 2n-1) diff --git a/app/core/src/main/resources/messages_bg_BG.properties b/app/core/src/main/resources/messages_bg_BG.properties index eaaeb5d95..3d3c89de0 100644 --- a/app/core/src/main/resources/messages_bg_BG.properties +++ b/app/core/src/main/resources/messages_bg_BG.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип цвят pdfToImage.color=Цвят pdfToImage.grey=Скала на сивото pdfToImage.blackwhite=Черно и бяло (може да загубите данни!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Преобразуване pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) diff --git a/app/core/src/main/resources/messages_bo_CN.properties b/app/core/src/main/resources/messages_bo_CN.properties index d84b4a387..b5aba5f6a 100644 --- a/app/core/src/main/resources/messages_bo_CN.properties +++ b/app/core/src/main/resources/messages_bo_CN.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=ཚོས་མདོག་གི་རིགས། pdfToImage.color=ཚོས་མདོག pdfToImage.grey=སྐྱ་མདོག pdfToImage.blackwhite=དཀར་ནག (གནས་ཚུལ་བརླག་སྲིད།) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=བསྒྱུར་བ། pdfToImage.info=Python སྒྲིག་འཇུག་བྱས་མི་འདུག WebP བསྒྱུར་བར་དགོས་མཁོ་ཡིན། pdfToImage.placeholder=(དཔེར་ན། 1,2,8 ཡང་ན་ 4,7,12-16 ཡང་ན་ 2n-1) diff --git a/app/core/src/main/resources/messages_ca_CA.properties b/app/core/src/main/resources/messages_ca_CA.properties index 30a7f7624..144c9bac1 100644 --- a/app/core/src/main/resources/messages_ca_CA.properties +++ b/app/core/src/main/resources/messages_ca_CA.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipus de Color pdfToImage.color=Color pdfToImage.grey=Escala de Grisos pdfToImage.blackwhite=Blanc i Negre (Pot perdre dades!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converteix pdfToImage.info=Python no està instal·lat. És necessari per a la conversió a WebP. pdfToImage.placeholder=(p. ex. 1,2,8 o 4,7,12-16 o 2n-1) diff --git a/app/core/src/main/resources/messages_cs_CZ.properties b/app/core/src/main/resources/messages_cs_CZ.properties index 7eb854aaa..e7b60818e 100644 --- a/app/core/src/main/resources/messages_cs_CZ.properties +++ b/app/core/src/main/resources/messages_cs_CZ.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Typ barev pdfToImage.color=Barevný pdfToImage.grey=Stupně šedi pdfToImage.blackwhite=Černobílý (Může dojít ke ztrátě dat!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Převést pdfToImage.info=Python není nainstalován. Vyžadován pro konverzi do WebP. pdfToImage.placeholder=(např. 1,2,8 nebo 4,7,12-16 nebo 2n-1) diff --git a/app/core/src/main/resources/messages_da_DK.properties b/app/core/src/main/resources/messages_da_DK.properties index 118e545e6..6b07c4a7d 100644 --- a/app/core/src/main/resources/messages_da_DK.properties +++ b/app/core/src/main/resources/messages_da_DK.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farvetype pdfToImage.color=Farve pdfToImage.grey=Gråtone pdfToImage.blackwhite=Sort og Hvid (Kan miste data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertér pdfToImage.info=Python er ikke installeret. Påkrævet for WebP-konvertering. pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1) diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index 5f01823f1..c78fbc31c 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farbtyp pdfToImage.color=Farbe pdfToImage.grey=Graustufen pdfToImage.blackwhite=Schwarzweiß (Datenverlust möglich!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Umwandeln pdfToImage.info=Python ist nicht installiert. Erforderlich für die WebP-Konvertierung. pdfToImage.placeholder=(z.B. 1,2,8 oder 4,7,12-16 oder 2n-1) diff --git a/app/core/src/main/resources/messages_el_GR.properties b/app/core/src/main/resources/messages_el_GR.properties index 82fc98ef9..773d873d2 100644 --- a/app/core/src/main/resources/messages_el_GR.properties +++ b/app/core/src/main/resources/messages_el_GR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Τύπος χρώματος pdfToImage.color=Έγχρωμο pdfToImage.grey=Κλίμακα του γκρι pdfToImage.blackwhite=Ασπρόμαυρο (Μπορεί να χαθούν δεδομένα!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Μετατροπή pdfToImage.info=Η Python δεν είναι εγκατεστημένη. Απαιτείται για μετατροπή WebP. pdfToImage.placeholder=(π.χ. 1,2,8 ή 4,7,12-16 ή 2n-1) diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index f78e80b65..a905a1f4d 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Colour type pdfToImage.color=Colour pdfToImage.grey=Greyscale pdfToImage.blackwhite=Black and White (May lose data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convert pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index eb8a9236c..34d5d81c4 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Color type pdfToImage.color=Color pdfToImage.grey=Grayscale pdfToImage.blackwhite=Black and White (May lose data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convert pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) diff --git a/app/core/src/main/resources/messages_es_ES.properties b/app/core/src/main/resources/messages_es_ES.properties index 5601e76c0..d6ba297c3 100644 --- a/app/core/src/main/resources/messages_es_ES.properties +++ b/app/core/src/main/resources/messages_es_ES.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo de color pdfToImage.color=Color pdfToImage.grey=Escala de grises pdfToImage.blackwhite=Blanco y Negro (¡Puede perder datos!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convertir pdfToImage.info=Python no está instalado. Se requiere para la conversión WebP. pdfToImage.placeholder=(por ejemplo 1,2,8 o 4,7,12-16 o 2n-1) diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index bda28227f..58f20132a 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Kolore-mota pdfToImage.color=Kolorea pdfToImage.grey=Gris-eskala pdfToImage.blackwhite=Zuria eta Beltza (Datuak galdu ditzake!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Bihurtu pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(e.g. 1,2,8 or 4,7,12-16 or 2n-1) diff --git a/app/core/src/main/resources/messages_fa_IR.properties b/app/core/src/main/resources/messages_fa_IR.properties index a1cfbe5b5..33a3baa7e 100644 --- a/app/core/src/main/resources/messages_fa_IR.properties +++ b/app/core/src/main/resources/messages_fa_IR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=نوع رنگ pdfToImage.color=رنگ pdfToImage.grey=خاکستری pdfToImage.blackwhite=سیاه و سفید (ممکن است اطلاعات از دست برود!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=تبدیل pdfToImage.info=پایتون نصب نشده است. برای تبدیل WebP لازم است. pdfToImage.placeholder=(مثال: 1,2,8 یا 4,7,12-16 یا 2n-1) diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index 2a3a7c6b5..4087137f8 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Type d'impression pdfToImage.color=Couleur pdfToImage.grey=Niveaux de gris pdfToImage.blackwhite=Noir et blanc (peut engendrer une perte de données !) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convertir pdfToImage.info=Python n'est pas installé. Nécessaire pour la conversion WebP. pdfToImage.placeholder=(par exemple : 1,2,8 ou 4,7,12-16 ou 2n-1) diff --git a/app/core/src/main/resources/messages_ga_IE.properties b/app/core/src/main/resources/messages_ga_IE.properties index 7a5d87de3..d90d14cc7 100644 --- a/app/core/src/main/resources/messages_ga_IE.properties +++ b/app/core/src/main/resources/messages_ga_IE.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Cineál dath pdfToImage.color=Dath pdfToImage.grey=Scála Liath pdfToImage.blackwhite=Dubh agus Bán (D’fhéadfadh sonraí a chailleadh!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Tiontaigh pdfToImage.info=Níl Python suiteáilte. Ag teastáil le haghaidh comhshó WebP. pdfToImage.placeholder=(m.sh. 1,2,8 nó 4,7,12-16 nó 2n-1) diff --git a/app/core/src/main/resources/messages_hi_IN.properties b/app/core/src/main/resources/messages_hi_IN.properties index ced3f3300..188de81f8 100644 --- a/app/core/src/main/resources/messages_hi_IN.properties +++ b/app/core/src/main/resources/messages_hi_IN.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=रंग प्रकार pdfToImage.color=रंग pdfToImage.grey=ग्रेस्केल pdfToImage.blackwhite=काला और सफेद (डेटा खो सकता है!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=बदलें pdfToImage.info=Python स्थापित नहीं है। WebP रूपांतरण के लिए आवश्यक है। pdfToImage.placeholder=(जैसे 1,2,8 या 4,7,12-16 या 2n-1) diff --git a/app/core/src/main/resources/messages_hr_HR.properties b/app/core/src/main/resources/messages_hr_HR.properties index f311a93dd..061f08497 100644 --- a/app/core/src/main/resources/messages_hr_HR.properties +++ b/app/core/src/main/resources/messages_hr_HR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tip boje pdfToImage.color=Boja pdfToImage.grey=Sivi tonovi pdfToImage.blackwhite=Crno-bijelo (mogu se izgubiti podaci!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Pretvori pdfToImage.info=Python nije instaliran. Treba je za konverziju na WebP. pdfToImage.placeholder=(t.j. 1,2,8 ili 4,7,12-16 ili 2n-1) diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 3132d4fdc..a4cd82005 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Színtípus pdfToImage.color=Színes pdfToImage.grey=Szürkeárnyalatos pdfToImage.blackwhite=Fekete-fehér (adatvesztéssel járhat!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertálás pdfToImage.info=Python nincs telepítve. WebP konverzióhoz szükséges. pdfToImage.placeholder=(pl. 1,2,8 vagy 4,7,12-16 vagy 2n-1) diff --git a/app/core/src/main/resources/messages_id_ID.properties b/app/core/src/main/resources/messages_id_ID.properties index 3a706535a..c75656b6e 100644 --- a/app/core/src/main/resources/messages_id_ID.properties +++ b/app/core/src/main/resources/messages_id_ID.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipe warna pdfToImage.color=Warna pdfToImage.grey=Skala abu-abu pdfToImage.blackwhite=Black and White (Bisa kehilangan data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konversi pdfToImage.info=Python tidak terinstal. Diperlukan untuk konversi WebP. pdfToImage.placeholder=(misalnya 1,2,8 atau 4,7,12-16 atau 2n-1) diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index bba74394b..be48d9e6f 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo di colore pdfToImage.color=A colori pdfToImage.grey=Scala di grigi pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converti pdfToImage.info=Python non è installato.È richiesto per la conversione WebP. pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1) diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index bdb30dc7b..12f490166 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=カラーモード pdfToImage.color=カラー pdfToImage.grey=グレースケール pdfToImage.blackwhite=白黒(データが失われる可能性があります!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=変換 pdfToImage.info=Pythonがインストールされていません。WebPの変換に必要です。 pdfToImage.placeholder=(例:1,2,8、4,7,12-16、2n-1) diff --git a/app/core/src/main/resources/messages_ko_KR.properties b/app/core/src/main/resources/messages_ko_KR.properties index 76f7ca715..70c4178d4 100644 --- a/app/core/src/main/resources/messages_ko_KR.properties +++ b/app/core/src/main/resources/messages_ko_KR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=색상 유형 pdfToImage.color=컬러 pdfToImage.grey=그레이스케일 pdfToImage.blackwhite=흑백 (데이터 손실 가능성 있음!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=변환 pdfToImage.info=WebP 변환에는 Python이 필요합니다. Python이 설치되지 않았습니다. pdfToImage.placeholder=(예: 1,2,8 또는 4,7,12-16 또는 2n-1) diff --git a/app/core/src/main/resources/messages_ml_IN.properties b/app/core/src/main/resources/messages_ml_IN.properties index 164209212..26d18fd4d 100644 --- a/app/core/src/main/resources/messages_ml_IN.properties +++ b/app/core/src/main/resources/messages_ml_IN.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=നിറ തരം pdfToImage.color=നിറം pdfToImage.grey=ഗ്രേസ്കെയിൽ pdfToImage.blackwhite=കറുപ്പും വെളുപ്പും (ഡാറ്റ നഷ്ടപ്പെട്ടേക്കാം!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=പരിവർത്തനം ചെയ്യുക pdfToImage.info=പൈത്തൺ ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല. WebP പരിവർത്തനത്തിന് ആവശ്യമാണ്. pdfToImage.placeholder=(ഉദാ. 1,2,8 അല്ലെങ്കിൽ 4,7,12-16 അല്ലെങ്കിൽ 2n-1) diff --git a/app/core/src/main/resources/messages_nl_NL.properties b/app/core/src/main/resources/messages_nl_NL.properties index 9a1af6e0a..ac001d2a8 100644 --- a/app/core/src/main/resources/messages_nl_NL.properties +++ b/app/core/src/main/resources/messages_nl_NL.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Kleurtype pdfToImage.color=Kleur pdfToImage.grey=Grijstinten pdfToImage.blackwhite=Zwart en wit (kan data verliezen!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Omzetten pdfToImage.info=Python is niet geïnstalleerd. Vereist voor WebP-conversie. pdfToImage.placeholder=(bijv. 1,2,8 of 4,7,12-16 of 2n-1) diff --git a/app/core/src/main/resources/messages_no_NB.properties b/app/core/src/main/resources/messages_no_NB.properties index 03d42816d..f16c1f898 100644 --- a/app/core/src/main/resources/messages_no_NB.properties +++ b/app/core/src/main/resources/messages_no_NB.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Farge type pdfToImage.color=Farge pdfToImage.grey=Gråtone pdfToImage.blackwhite=Svart-hvitt (kan miste data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konverter pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(f.eks. 1,2,8 eller 4,7,12-16 eller 2n-1) diff --git a/app/core/src/main/resources/messages_pl_PL.properties b/app/core/src/main/resources/messages_pl_PL.properties index 4963d7bc6..442540fbb 100644 --- a/app/core/src/main/resources/messages_pl_PL.properties +++ b/app/core/src/main/resources/messages_pl_PL.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Rodzaj koloru pdfToImage.color=Kolor pdfToImage.grey=Odcień szarości pdfToImage.blackwhite=Czarno-biały (może spowodować utratę danych!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konwertuj pdfToImage.info=Python nie został zainstalowany. Jest wymagany do konwersji WebP. pdfToImage.placeholder=(przykład 1,2,8 lub 2n-1) diff --git a/app/core/src/main/resources/messages_pt_BR.properties b/app/core/src/main/resources/messages_pt_BR.properties index 552139b52..5db475798 100644 --- a/app/core/src/main/resources/messages_pt_BR.properties +++ b/app/core/src/main/resources/messages_pt_BR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Cor de saída: pdfToImage.color=Colorido pdfToImage.grey=Escala de Cinza pdfToImage.blackwhite=Preto e Branco (pode perder informações!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converter pdfToImage.info=Python não está instalado. Necessário para conversão WebP. pdfToImage.placeholder=(por exemplo 1,2,8 ou 4,7,12-16 ou 2n-1) diff --git a/app/core/src/main/resources/messages_pt_PT.properties b/app/core/src/main/resources/messages_pt_PT.properties index dc99a45c9..aad725305 100644 --- a/app/core/src/main/resources/messages_pt_PT.properties +++ b/app/core/src/main/resources/messages_pt_PT.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tipo de cor pdfToImage.color=Cor pdfToImage.grey=Escala de Cinza pdfToImage.blackwhite=Preto e Branco (Pode perder dados!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Converter pdfToImage.info=Python não está instalado. Necessário para conversão WebP. pdfToImage.placeholder=(ex. 1,2,8 ou 4,7,12-16 ou 2n-1) diff --git a/app/core/src/main/resources/messages_ro_RO.properties b/app/core/src/main/resources/messages_ro_RO.properties index 25bc477b1..d785d49c6 100644 --- a/app/core/src/main/resources/messages_ro_RO.properties +++ b/app/core/src/main/resources/messages_ro_RO.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Tip culoare pdfToImage.color=Culoare pdfToImage.grey=Scală de gri pdfToImage.blackwhite=Alb și negru (Poate pierde date!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Convertește pdfToImage.info=Python nu este instalat. Necesar pentru conversia WebP. pdfToImage.placeholder=(ex. 1,2,8 sau 4,7,12-16 sau 2n-1) diff --git a/app/core/src/main/resources/messages_ru_RU.properties b/app/core/src/main/resources/messages_ru_RU.properties index e414408f1..64bb21a2c 100644 --- a/app/core/src/main/resources/messages_ru_RU.properties +++ b/app/core/src/main/resources/messages_ru_RU.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип цвета pdfToImage.color=Цветной pdfToImage.grey=Оттенки серого pdfToImage.blackwhite=Черно-белый (возможна потеря данных!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Преобразовать pdfToImage.info=Python не установлен. Требуется для конвертации в WebP. pdfToImage.placeholder=(например, 1,2,8 или 4,7,12-16 или 2n-1) diff --git a/app/core/src/main/resources/messages_sk_SK.properties b/app/core/src/main/resources/messages_sk_SK.properties index 094265d29..6325a85e9 100644 --- a/app/core/src/main/resources/messages_sk_SK.properties +++ b/app/core/src/main/resources/messages_sk_SK.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Typ farby pdfToImage.color=Farba pdfToImage.grey=Odtiene šedej pdfToImage.blackwhite=Čierno-biele (Môže stratiť údaje!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertovať pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(napr. 1,2,8 alebo 4,7,12-16 alebo 2n-1) diff --git a/app/core/src/main/resources/messages_sl_SI.properties b/app/core/src/main/resources/messages_sl_SI.properties index f75e714af..698e5aee0 100644 --- a/app/core/src/main/resources/messages_sl_SI.properties +++ b/app/core/src/main/resources/messages_sl_SI.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Vrsta barve pdfToImage.color=Barva pdfToImage.grey=Sivine pdfToImage.blackwhite=Črno-belo (Lahko izgubite podatke!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Pretvori pdfToImage.info=Python ni nameščen. Zahtevano za pretvorbo WebP. pdfToImage.placeholder=(npr. 1,2,8 ali 4,7,12-16 ali 2n-1) diff --git a/app/core/src/main/resources/messages_sr_LATN_RS.properties b/app/core/src/main/resources/messages_sr_LATN_RS.properties index 809a785ee..968c277e9 100644 --- a/app/core/src/main/resources/messages_sr_LATN_RS.properties +++ b/app/core/src/main/resources/messages_sr_LATN_RS.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Režim boja: pdfToImage.color=Kolor pdfToImage.grey=Monohromatski pdfToImage.blackwhite=Crno-belo (Može izgubiti detalje!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertuj pdfToImage.info=Python nije instaliran. Neophodan je za WebP konverziju. pdfToImage.placeholder=(npr. 1,2,8 ili 4,7,12-16 ili 2n-1) diff --git a/app/core/src/main/resources/messages_sv_SE.properties b/app/core/src/main/resources/messages_sv_SE.properties index 086789fcd..31c250caa 100644 --- a/app/core/src/main/resources/messages_sv_SE.properties +++ b/app/core/src/main/resources/messages_sv_SE.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Färgtyp pdfToImage.color=Färg pdfToImage.grey=Gråskala pdfToImage.blackwhite=Svartvitt (kan förlora data!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Konvertera pdfToImage.info=Python är inte installerat. Krävs för WebP-konvertering. pdfToImage.placeholder=(t.ex. 1,2,8 eller 4,7,12-16 eller 2n-1) diff --git a/app/core/src/main/resources/messages_th_TH.properties b/app/core/src/main/resources/messages_th_TH.properties index 2637eadf2..df50423ca 100644 --- a/app/core/src/main/resources/messages_th_TH.properties +++ b/app/core/src/main/resources/messages_th_TH.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=ประเภทสี pdfToImage.color=สี pdfToImage.grey=ระดับสีเทา pdfToImage.blackwhite=ขาวดำ (อาจสูญเสียข้อมูล!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=แปลง pdfToImage.info=Python ไม่มีการติดตั้ง จำเป็นสำหรับการแปลง WebP pdfToImage.placeholder=(เช่น 1,2,8 หรือ 4,7,12-16 หรือ 2n-1) diff --git a/app/core/src/main/resources/messages_tr_TR.properties b/app/core/src/main/resources/messages_tr_TR.properties index 2da6244ff..01e946c56 100644 --- a/app/core/src/main/resources/messages_tr_TR.properties +++ b/app/core/src/main/resources/messages_tr_TR.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Renk türü pdfToImage.color=Renk pdfToImage.grey=Gri tonlama pdfToImage.blackwhite=Siyah ve Beyaz (Veri kaybolabilir!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Dönüştür pdfToImage.info=Python kurulu değil. WebP dönüşümü için gereklidir. pdfToImage.placeholder=(örneğin 1,2,8 veya 4,7,12-16 ya da 2n-1) diff --git a/app/core/src/main/resources/messages_uk_UA.properties b/app/core/src/main/resources/messages_uk_UA.properties index 89518dca8..16add9977 100644 --- a/app/core/src/main/resources/messages_uk_UA.properties +++ b/app/core/src/main/resources/messages_uk_UA.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Тип кольору pdfToImage.color=Колір pdfToImage.grey=Відтінки сірого pdfToImage.blackwhite=Чорно-білий (може втратити дані!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Конвертувати pdfToImage.info=Python не встановлено. Необхідно для конвертації WebP. pdfToImage.placeholder=(наприклад 1,2,8 або 4,7,12-16 або 2n-1) diff --git a/app/core/src/main/resources/messages_vi_VN.properties b/app/core/src/main/resources/messages_vi_VN.properties index dcf69cce8..c9e070e77 100644 --- a/app/core/src/main/resources/messages_vi_VN.properties +++ b/app/core/src/main/resources/messages_vi_VN.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=Loại màu pdfToImage.color=Màu pdfToImage.grey=Thang độ xám pdfToImage.blackwhite=Đen trắng (Có thể mất dữ liệu!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=Chuyển đổi pdfToImage.info=Python is not installed. Required for WebP conversion. pdfToImage.placeholder=(ví dụ: 1,2,8 hoặc 4,7,12-16 hoặc 2n-1) diff --git a/app/core/src/main/resources/messages_zh_CN.properties b/app/core/src/main/resources/messages_zh_CN.properties index 247b6ea70..75957b1b0 100644 --- a/app/core/src/main/resources/messages_zh_CN.properties +++ b/app/core/src/main/resources/messages_zh_CN.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=颜色类型 pdfToImage.color=颜色 pdfToImage.grey=灰度 pdfToImage.blackwhite=黑白(可能会丢失数据!)。 +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=转换 pdfToImage.info=WebP 转换需要安装 Python pdfToImage.placeholder=(例如:1,2,8 或 4,7,12-16 或 2n-1) diff --git a/app/core/src/main/resources/messages_zh_TW.properties b/app/core/src/main/resources/messages_zh_TW.properties index bbd3cd495..3968b07d2 100644 --- a/app/core/src/main/resources/messages_zh_TW.properties +++ b/app/core/src/main/resources/messages_zh_TW.properties @@ -1402,6 +1402,7 @@ pdfToImage.colorType=顏色類型 pdfToImage.color=顏色 pdfToImage.grey=灰度 pdfToImage.blackwhite=黑白(可能會遺失資料!) +pdfToImage.dpi=DPI (The server limit is {0} dpi) pdfToImage.submit=轉換 pdfToImage.info=尚未安裝 Python。需要安裝 Python 才能進行 WebP 轉換。 pdfToImage.placeholder=(例如 1,2,8 或 4,7,12-16 或 2n-1) diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index cf22262e4..1af95f852 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -108,6 +108,7 @@ system: enableAnalytics: null # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML) + maxDPI: 500 # Maximum allowed DPI for PDF to image conversion html: urlSecurity: enabled: true # Enable URL security restrictions for HTML processing diff --git a/app/core/src/main/resources/templates/convert/pdf-to-img.html b/app/core/src/main/resources/templates/convert/pdf-to-img.html index b4f2b0657..78c7ca901 100644 --- a/app/core/src/main/resources/templates/convert/pdf-to-img.html +++ b/app/core/src/main/resources/templates/convert/pdf-to-img.html @@ -4,6 +4,21 @@ + diff --git a/devGuide/DeveloperGuide.md b/devGuide/DeveloperGuide.md index c37be9b84..fb8911eaf 100644 --- a/devGuide/DeveloperGuide.md +++ b/devGuide/DeveloperGuide.md @@ -295,6 +295,7 @@ Stirling-PDF can be customized through environment variables or a `settings.yml` - Security settings - UI customization - Endpoint management +- Maximum DPI for PDF to image conversion (`system.maxDPI`) When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file. From 6cd64a22ba42f42d19e34c32f0dca0ce0ada9dba Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 11:36:30 +0200 Subject: [PATCH 02/23] build(local): simplify writeVersion task with WriteProperties plugin and enable build caching (#4139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What was changed**: - Replaced the custom `writeVersion` task in `build.gradle` with the built-in `WriteProperties` plugin configuration. - Updated `gradle.properties` to enable `org.gradle.caching` (uncommented) for local development. - **Why the change was made**: - To reduce boilerplate and leverage Gradle’s native property-writing capabilities for maintaining the version file. - To improve build performance by reusing outputs via the Gradle build cache. - **Scope**: - These updates only affect local development and do not change production or CI script --- ## 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/devGuide/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/devGuide/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) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- build.gradle | 27 +++++---------------------- gradle.properties | 3 ++- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index 627d7b5c1..fd9abf7c8 100644 --- a/build.gradle +++ b/build.gradle @@ -65,28 +65,11 @@ allprojects { } } -tasks.register('writeVersion') { - def propsFile = file("$projectDir/app/common/src/main/resources/version.properties") - def propsDir = propsFile.parentFile - - doLast { - if (propsDir.exists()) { - if (propsFile.exists()) { - println "File exists: $propsFile" - } else { - println "$propsFile does not exist. Creating file." - propsFile.createNewFile() - } - } else { - println "Creating directory: $propsDir" - propsDir.mkdirs() - propsFile.createNewFile() - } - - def props = new Properties() - props.setProperty("version", version) - props.store(propsFile.newWriter(), null) - } +tasks.register('writeVersion', WriteProperties) { + outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties') + println "Writing version.properties to ${outputFile.path}" + comment "${new Date()}" + property 'version', project.provider { project.version.toString() } } tasks.named('createExe') { diff --git a/gradle.properties b/gradle.properties index 9184cf5c6..8a390f592 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,7 @@ org.gradle.parallel=true # Enables build caching to reuse outputs from previous builds for faster execution -# org.gradle.caching=true +org.gradle.caching=true org.gradle.build-scan=true +# org.gradle.configuration-cache=true From c4c9f3f3032c1259d33380c427131db4dc44ab5b Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:38:57 +0100 Subject: [PATCH 03/23] Update CODEOWNERS (#4142) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/CODEOWNERS | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8d4e98e5a..7d5389fda 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,21 @@ -# All PRs to V1 must be approved by Frooodle -* @Frooodle @reecebrowne @Ludy87 @DarioGii @ConnorYoh @EthanHealy01 +# All PRs must be approved by Frooodle or Ludy87 +* @Frooodle @Ludy87 @jbrunton96 @ConnorYoh + +# Backend +/app/** @DarioGii + +#V1 frontend +/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 + +#V2 frontend +/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 + +#V2 docker +/docker/backend/** @Frooodle @Ludy87 @DarioGii +/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 + + +#GHA (All users) +/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 From d3c786d018293021f483cdfb8afcef62070335d4 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:21:29 +0100 Subject: [PATCH 04/23] :globe_with_meridians: Sync Translations + Update README Progress Table (#4135) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0a563fa5..b9660ce43 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Stirling-PDF currently supports 40 languages! | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) | -| German (Deutsch) (de_DE) | ![100%](https://geps.dev/progress/100) | +| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) | | Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) | | Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | From b77d02e9884376d8e44a8e5cfa4a3193b9677bda Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 13:30:30 +0200 Subject: [PATCH 05/23] chore(templates): remove redundant `fetch-utils.js` script includes (#4092) # Description of Changes - **What was changed**: Removed all explicit `` tags from various Thymeleaf templates (`home.html`, `home-legacy.html`, `scanner-effect.html`, etc.). - **Why the change was made**: The `fetch-utils.js` script is already included globally via `` in `fragments/common.html` (line 156). Keeping redundant includes leads to unnecessary script loading and potential duplication. --- ## 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/devGuide/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/devGuide/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) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- app/core/src/main/resources/templates/home-legacy.html | 3 --- app/core/src/main/resources/templates/home.html | 1 - app/core/src/main/resources/templates/misc/scanner-effect.html | 1 - .../src/main/resources/templates/misc/show-javascript.html | 1 - app/core/src/main/resources/templates/pipeline.html | 1 - .../src/main/resources/templates/security/get-info-on-pdf.html | 1 - 6 files changed, 8 deletions(-) diff --git a/app/core/src/main/resources/templates/home-legacy.html b/app/core/src/main/resources/templates/home-legacy.html index 9531a359b..3c01bcbd6 100644 --- a/app/core/src/main/resources/templates/home-legacy.html +++ b/app/core/src/main/resources/templates/home-legacy.html @@ -413,9 +413,6 @@ - - - diff --git a/app/core/src/main/resources/templates/security/get-info-on-pdf.html b/app/core/src/main/resources/templates/security/get-info-on-pdf.html index 86e65cd01..0b64bb679 100644 --- a/app/core/src/main/resources/templates/security/get-info-on-pdf.html +++ b/app/core/src/main/resources/templates/security/get-info-on-pdf.html @@ -106,7 +106,6 @@ - - + - + \ No newline at end of file From b91bfac41667c09bdd0979f07ca15eba9b025a38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:50:21 +0100 Subject: [PATCH 07/23] build(deps): bump docker/login-action from 3.4.0 to 3.5.0 (#4118) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [docker/login-action](https://github.com/docker/login-action) from 3.4.0 to 3.5.0.
Release notes

Sourced from docker/login-action's releases.

v3.5.0

Full Changelog: https://github.com/docker/login-action/compare/v3.4.0...v3.5.0

Commits
  • 184bdaa Merge pull request #878 from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...
  • 5c6bc94 chore: update generated content
  • caf4058 build(deps): bump the aws-sdk-dependencies group with 2 updates
  • ef38ec3 Merge pull request #860 from docker/dependabot/npm_and_yarn/aws-sdk-dependenc...
  • d52e8ef chore: update generated content
  • 9644ab7 build(deps): bump the aws-sdk-dependencies group with 2 updates
  • 7abd1d5 Merge pull request #875 from docker/dependabot/npm_and_yarn/form-data-2.5.5
  • 1a81202 Merge pull request #876 from crazy-max/aws-public-dual-stack
  • d1ab30d chore: update generated content
  • f25ff28 support dual-stack for aws public ecr
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=docker/login-action&package-manager=github_actions&previous-version=3.4.0&new-version=3.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/PR-Demo-Comment-with-react.yml | 2 +- .github/workflows/push-docker.yml | 4 ++-- .github/workflows/testdriver.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/PR-Demo-Comment-with-react.yml b/.github/workflows/PR-Demo-Comment-with-react.yml index 013db2886..066d85ef2 100644 --- a/.github/workflows/PR-Demo-Comment-with-react.yml +++ b/.github/workflows/PR-Demo-Comment-with-react.yml @@ -196,7 +196,7 @@ jobs: uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index dbbc2622d..2a04ba33e 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -67,13 +67,13 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index cdb8b345d..b5759ed54 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -57,7 +57,7 @@ jobs: echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 + uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} From bb8edffaabc4196cfc300bbbd0d849eb2ac7e3cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:50:30 +0100 Subject: [PATCH 08/23] build(deps): bump actions/ai-inference from 1.2.3 to 1.2.4 (#4119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/ai-inference](https://github.com/actions/ai-inference) from 1.2.3 to 1.2.4.
Release notes

Sourced from actions/ai-inference's releases.

v1.2.4

What's Changed

Full Changelog: https://github.com/actions/ai-inference/compare/v1...v1.2.4

Commits
  • 4b591cc Merge pull request #83 from actions/sgoedecke/separate-mcp
  • ea24ec2 Update README.md
  • b9f9444 update docs
  • 419f171 Separate out MCP token
  • fc8527d Merge pull request #74 from actions/dependabot/github_actions/actions-minor-e...
  • 719349d Merge branch 'main' into dependabot/github_actions/actions-minor-e893b3f303
  • 2762750 Merge pull request #76 from actions/dependabot/npm_and_yarn/rollup/rollup-lin...
  • 9386906 chore(deps): bump @​rollup/rollup-linux-x64-gnu from 4.45.1 to 4.46.0
  • ca9eff7 chore(deps): bump actions/publish-action in the actions-minor group
  • 6bef1d0 Merge pull request #72 from actions/mr/linters
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/ai-inference&package-manager=github_actions&previous-version=1.2.3&new-version=1.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ai_pr_title_review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ai_pr_title_review.yml b/.github/workflows/ai_pr_title_review.yml index b7d944c34..8a2e8b8ef 100644 --- a/.github/workflows/ai_pr_title_review.yml +++ b/.github/workflows/ai_pr_title_review.yml @@ -87,7 +87,7 @@ jobs: - name: AI PR Title Analysis if: steps.actor.outputs.is_repo_dev == 'true' id: ai-title-analysis - uses: actions/ai-inference@9693b137b6566bb66055a713613bf4f0493701eb # v1.2.3 + uses: actions/ai-inference@0cbed4a10641c75090de5968e66d70eb4660f751 # v1.2.7 with: model: openai/gpt-4o system-prompt-file: ".github/config/system-prompt.txt" From b6ff1dd7f60f0b98ec5235e41775c38d67b99222 Mon Sep 17 00:00:00 2001 From: Ludy Date: Fri, 8 Aug 2025 13:52:51 +0200 Subject: [PATCH 09/23] chore: update development configs, formatting tools, and CI enhancements (#4130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes - **What was changed** - Bumped `java.format.settings.google.version` to **1.28.0** in `.devcontainer/devcontainer.json` and `.vscode/settings.json`. - Expanded ignore patterns in `.devcontainer/devcontainer.json` to cover `app/core/`, `app/common/`, `app/proprietary/` directories. - Added a new top‐level `.dockerignore` to exclude build artifacts, virtual environments, logs, OS files, and markdown docs. - Consolidated EditorConfig YAML globs into `*.{yml,yaml}` to remove duplication. - Fixed missing newline in `.github/config/.files.yaml` and added label metadata (`from_name`/`description`) in `.github/labels.yml`. - Updated `build.gradle`: - Introduced `junitPlatformVersion = "1.12.2"` and replaced hard-coded launcher versions. - Applied the `jacoco` plugin across all subprojects and configured `jacocoTestReport` (XML + HTML). - Wire-up `jacocoTestReport` to run after tests. - **Why the change was made** - Ensure all formatting tools (Google Java Format) stay in sync across editors and containers. - Clean up ignore rules to prevent build artifacts and sensitive files from creeping into images and repos. - Improve CI visibility by generating code-coverage reports with JaCoCo. - Keep GitHub configuration files well-formed and enrich label definitions for automation. --- ## 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/devGuide/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/devGuide/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) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .devcontainer/devcontainer.json | 13 ++++++++++++- .editorconfig | 8 +------- .github/config/.files.yaml | 2 +- .github/labels.yml | 5 +++++ .vscode/settings.json | 3 ++- build.gradle | 16 ++++++++++++++-- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5ab9f82c9..dcc0ca600 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -49,7 +49,7 @@ "java.configuration.updateBuildConfiguration": "interactive", "java.format.enabled": true, "java.format.settings.profile": "GoogleStyle", - "java.format.settings.google.version": "1.26.0", + "java.format.settings.google.version": "1.28.0", "java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting", "java.saveActions.cleanup": true, "java.cleanup.actions": [ @@ -79,9 +79,17 @@ ".venv*/", ".vscode/", "bin/", + "app/core/bin/", + "app/common/bin/", + "app/proprietary/bin/", "build/", + "app/core/build/", + "app/common/build/", + "app/proprietary/build/", "configs/", + "app/core/configs/", "customFiles/", + "app/core/customFiles/", "docs/", "exampleYmlFiles", "gradle/", @@ -93,6 +101,9 @@ ".git-blame-ignore-revs", ".gitattributes", ".gitignore", + "app/core/.gitignore", + "app/common/.gitignore", + "app/proprietary/.gitignore", ".pre-commit-config.yaml" ], "java.signatureHelp.enabled": true, diff --git a/.editorconfig b/.editorconfig index d45455a7a..3f5158dea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -31,18 +31,12 @@ indent_size = 2 # CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards. indent_size = 2 -[*.yaml] +[*.{yml,yaml}] # YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices. indent_size = 2 insert_final_newline = false trim_trailing_whitespace = false -[*.yml] -# YML files follow the same conventions as YAML files, using an indent size of 2 spaces. -indent_size = 2 -insert_final_newline = false -trim_trailing_whitespace = false - [*.json] # JSON files use an indent size of 2 spaces, which is the standard for JSON formatting. indent_size = 2 diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 2f4f242cb..225470ea9 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -26,4 +26,4 @@ project: &project - gradlew - gradlew.bat - launch4jConfig.xml - - settings.gradle \ No newline at end of file + - settings.gradle diff --git a/.github/labels.yml b/.github/labels.yml index 9b35ccb1a..b6cd969f6 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -42,6 +42,7 @@ - name: "Front End" color: "BBD2F1" description: "Issues or pull requests related to front-end development" + from_name: "frontend" - name: "github-actions" description: "Pull requests that update GitHub Actions code" color: "999999" @@ -77,6 +78,7 @@ - name: "Translation" color: "9FABF9" from_name: "translation" + description: "Issues or pull requests related to translation" - name: "upstream" color: "DEDEDE" - name: "v2" @@ -178,3 +180,6 @@ - name: "pr-deployed" color: "00FF00" description: "Pull request has been deployed to a test environment" +- name: "codex" + color: "ededed" + description: "chatgpt AI generated code" \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index abc54d43e..5b8f77bbc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "editor.wordSegmenterLocales": "", "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", + "editor.defaultFormatter": "EditorConfig.EditorConfig", "cSpell.enabled": false, "[feature]": { "editor.defaultFormatter": "alexkrechik.cucumberautocomplete" @@ -40,7 +41,7 @@ "java.configuration.updateBuildConfiguration": "interactive", "java.format.enabled": true, "java.format.settings.profile": "GoogleStyle", - "java.format.settings.google.version": "1.27.0", + "java.format.settings.google.version": "1.28.0", "java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting", // (DE) Aktiviert Kommentare im Java-Format. // (EN) Enables comments in Java formatting. diff --git a/build.gradle b/build.gradle index fd9abf7c8..ec786e2ed 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ ext { openSamlVersion = "4.3.2" commonmarkVersion = "0.25.1" googleJavaFormatVersion = "1.28.0" + junitPlatformVersion = "1.12.2" tempJrePath = null } @@ -82,6 +83,7 @@ subprojects { apply plugin: 'com.diffplug.spotless' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' + apply plugin: 'jacoco' java { // 17 is lowest but we support and recommend 21 @@ -125,7 +127,7 @@ subprojects { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' + testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" } tasks.withType(JavaCompile).configureEach { @@ -139,6 +141,16 @@ subprojects { test { useJUnitPlatform() + finalizedBy jacocoTestReport + } + + jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } } tasks.named("processResources") { @@ -556,7 +568,7 @@ dependencies { } testImplementation 'org.springframework.boot:spring-boot-starter-test' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2' + testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" } tasks.named("test") { From 65e894870c07148d9a22d27e9ae962f3679fae83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:14:57 +0200 Subject: [PATCH 10/23] refactor(eml-to-pdf): Improve readability, maintainability, and overall standards compliance (#4065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description of Changes refactor(eml-to-pdf): Enhance compliance with PDF/ISO standards and MIME specifications This commit refactors the EML-to-PDF conversion utility to improve standards compliance, implementing requirements from multiple RFCs and ISO specifications: ### Standards Compliance Implemented: • **PDF Standards (ISO 32000-1:2008)**: Added PDF version validation in `attachFilesToPdf()` to ensure 1.7+ compatibility for Unicode file embeddings • **MIME Processing (RFC 2045/2046)**: Implemented case-insensitive MIME type handling in `processPartAdvanced()` with `toLowerCase(Locale.ROOT)` normalization • **Content Encoding (RFC 2047)**: Enhanced `safeMimeDecode()` with UTF-8→ISO-8859-1 charset fallback chains for robust header decoding • **Content-ID Processing (RFC 2392)**: Added proper Content-ID stripping with `replaceAll("[<>]", "")` for embedded image references • **Multipart Safety (RFC 2046)** (best practice, not compliance related): Implemented recursion depth limiting (max 10 levels) • **processMultipartAdvanced()**, setCatalogViewerPreferences used to set PageMode.USE_ATTACHMENTS, but PDF spec 12.2 (Viewer Preferences) requires a /ViewerPreferences dictionary for full control (e.g., /DisplayDocTitle). Docs suggested setting additional prefs like /NonFullScreenPageMode to ensure attachments panel opens reliably across viewers • **addAttachmentAnnotationToPage**, annotations are set to /Invisible=true but must remain interactive. PDF spec 12.5.6.15 (File Attachment Annotations) requires /F flags to control print/view (e.g., NoPrint if not printable). ### Technical Improvements: • **Coordinate System Handling**: Added rotation-aware coordinate transformations in PDF annotation placement following ISO 32000-1 Section 8.3 • **Charset Fallbacks**: Implemented progressive charset detection with UTF-8 primary and ISO-8859-1 fallback in MIME decoding • **Error Resilience**: Enhanced exception handling with specific error types and proper resource cleanup using try-with-resources patterns • **HTML5 Compliance**: Updated email HTML generation with proper DOCTYPE and charset declarations for browser compatibility ### Security & Robustness: • **Input Validation**: Added comprehensive null checks and boundary validation throughout attachment and multipart processing • **XSS Prevention**: All user content now processed through `escapeHtml()` or `CustomHtmlSanitizer` before HTML generation ### Code Quality: • **Method Signatures**: Updated `processMultipartAdvanced()` to include depth parameter for recursion tracking • **Switch Expressions**: Modernized switch statements to use Java 17+ arrow syntax where applicable • **Documentation**: Added inline RFC/ISO references for compliance-critical sections All changes maintain backward compatibility while significantly improving standards adherence. Tested with various EML formats. No major change. No change in tests. No change in aesthetic of the resulting PDF. No change change in "user space" (except when user relied on compliance of aforementioned stuff then a major improvement) --- ## 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/devGuide/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/devGuide/HowToAddNewLanguage.md) (if applicable) - [x] 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/devGuide/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) - [x] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../software/common/util/EmlParser.java | 652 +++++++ .../common/util/EmlProcessingUtils.java | 601 ++++++ .../software/common/util/EmlToPdf.java | 1655 +---------------- .../common/util/PdfAttachmentHandler.java | 680 +++++++ 4 files changed, 1950 insertions(+), 1638 deletions(-) create mode 100644 app/common/src/main/java/stirling/software/common/util/EmlParser.java create mode 100644 app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java create mode 100644 app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java diff --git a/app/common/src/main/java/stirling/software/common/util/EmlParser.java b/app/common/src/main/java/stirling/software/common/util/EmlParser.java new file mode 100644 index 000000000..0815b1c56 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/EmlParser.java @@ -0,0 +1,652 @@ +package stirling.software.common.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; +import java.util.regex.Pattern; + +import lombok.Data; +import lombok.experimental.UtilityClass; + +import stirling.software.common.model.api.converters.EmlToPdfRequest; + +@UtilityClass +public class EmlParser { + + private static volatile Boolean jakartaMailAvailable = null; + private static volatile Method mimeUtilityDecodeTextMethod = null; + private static volatile boolean mimeUtilityChecked = false; + + private static final Pattern MIME_ENCODED_PATTERN = + Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + + private static final String DISPOSITION_ATTACHMENT = "attachment"; + private static final String TEXT_PLAIN = "text/plain"; + private static final String TEXT_HTML = "text/html"; + private static final String MULTIPART_PREFIX = "multipart/"; + + private static final String HEADER_CONTENT_TYPE = "content-type:"; + private static final String HEADER_CONTENT_DISPOSITION = "content-disposition:"; + private static final String HEADER_CONTENT_TRANSFER_ENCODING = "content-transfer-encoding:"; + private static final String HEADER_CONTENT_ID = "Content-ID"; + private static final String HEADER_SUBJECT = "Subject:"; + private static final String HEADER_FROM = "From:"; + private static final String HEADER_TO = "To:"; + private static final String HEADER_CC = "Cc:"; + private static final String HEADER_BCC = "Bcc:"; + private static final String HEADER_DATE = "Date:"; + + private static synchronized boolean isJakartaMailAvailable() { + if (jakartaMailAvailable == null) { + try { + Class.forName("jakarta.mail.internet.MimeMessage"); + Class.forName("jakarta.mail.Session"); + Class.forName("jakarta.mail.internet.MimeUtility"); + Class.forName("jakarta.mail.internet.MimePart"); + Class.forName("jakarta.mail.internet.MimeMultipart"); + Class.forName("jakarta.mail.Multipart"); + Class.forName("jakarta.mail.Part"); + jakartaMailAvailable = true; + } catch (ClassNotFoundException e) { + jakartaMailAvailable = false; + } + } + return jakartaMailAvailable; + } + + public static EmailContent extractEmailContent( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) + throws IOException { + EmlProcessingUtils.validateEmlInput(emlBytes); + + if (isJakartaMailAvailable()) { + return extractEmailContentAdvanced(emlBytes, request, customHtmlSanitizer); + } else { + return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + } + } + + private static EmailContent extractEmailContentBasic( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + String emlContent = new String(emlBytes, StandardCharsets.UTF_8); + EmailContent content = new EmailContent(); + + content.setSubject(extractBasicHeader(emlContent, HEADER_SUBJECT)); + content.setFrom(extractBasicHeader(emlContent, HEADER_FROM)); + content.setTo(extractBasicHeader(emlContent, HEADER_TO)); + content.setCc(extractBasicHeader(emlContent, HEADER_CC)); + content.setBcc(extractBasicHeader(emlContent, HEADER_BCC)); + + String dateStr = extractBasicHeader(emlContent, HEADER_DATE); + if (!dateStr.isEmpty()) { + content.setDateString(dateStr); + } + + String htmlBody = extractHtmlBody(emlContent); + if (htmlBody != null) { + content.setHtmlBody(htmlBody); + } else { + String textBody = extractTextBody(emlContent); + content.setTextBody(textBody != null ? textBody : "Email content could not be parsed"); + } + + content.getAttachments().addAll(extractAttachmentsBasic(emlContent)); + + return content; + } + + private static EmailContent extractEmailContentAdvanced( + byte[] emlBytes, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + try { + Class sessionClass = Class.forName("jakarta.mail.Session"); + Class mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage"); + + Method getDefaultInstance = + sessionClass.getMethod("getDefaultInstance", Properties.class); + Object session = getDefaultInstance.invoke(null, new Properties()); + + Class[] constructorArgs = new Class[] {sessionClass, InputStream.class}; + Constructor mimeMessageConstructor = + mimeMessageClass.getConstructor(constructorArgs); + Object message = + mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes)); + + return extractFromMimeMessage(message, request, customHtmlSanitizer); + + } catch (ReflectiveOperationException e) { + return extractEmailContentBasic(emlBytes, request, customHtmlSanitizer); + } + } + + private static EmailContent extractFromMimeMessage( + Object message, EmlToPdfRequest request, CustomHtmlSanitizer customHtmlSanitizer) { + EmailContent content = new EmailContent(); + + try { + Class messageClass = message.getClass(); + + Method getSubject = messageClass.getMethod("getSubject"); + String subject = (String) getSubject.invoke(message); + content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject"); + + Method getFrom = messageClass.getMethod("getFrom"); + Object[] fromAddresses = (Object[]) getFrom.invoke(message); + content.setFrom(buildAddressString(fromAddresses)); + + extractRecipients(message, messageClass, content); + + Method getSentDate = messageClass.getMethod("getSentDate"); + content.setDate((Date) getSentDate.invoke(message)); + + Method getContent = messageClass.getMethod("getContent"); + Object messageContent = getContent.invoke(message); + + processMessageContent(message, messageContent, content, request, customHtmlSanitizer); + + } catch (ReflectiveOperationException | RuntimeException e) { + content.setSubject("Email Conversion"); + content.setFrom("Unknown"); + content.setTo("Unknown"); + content.setCc(""); + content.setBcc(""); + content.setTextBody("Email content could not be parsed with advanced processing"); + } + + return content; + } + + private static void extractRecipients( + Object message, Class messageClass, EmailContent content) { + try { + Method getRecipients = + messageClass.getMethod( + "getRecipients", Class.forName("jakarta.mail.Message$RecipientType")); + Class recipientTypeClass = Class.forName("jakarta.mail.Message$RecipientType"); + + Object toType = recipientTypeClass.getField("TO").get(null); + Object[] toRecipients = (Object[]) getRecipients.invoke(message, toType); + content.setTo(buildAddressString(toRecipients)); + + Object ccType = recipientTypeClass.getField("CC").get(null); + Object[] ccRecipients = (Object[]) getRecipients.invoke(message, ccType); + content.setCc(buildAddressString(ccRecipients)); + + Object bccType = recipientTypeClass.getField("BCC").get(null); + Object[] bccRecipients = (Object[]) getRecipients.invoke(message, bccType); + content.setBcc(buildAddressString(bccRecipients)); + + } catch (ReflectiveOperationException e) { + try { + Method getAllRecipients = messageClass.getMethod("getAllRecipients"); + Object[] recipients = (Object[]) getAllRecipients.invoke(message); + content.setTo(buildAddressString(recipients)); + content.setCc(""); + content.setBcc(""); + } catch (ReflectiveOperationException ex) { + content.setTo(""); + content.setCc(""); + content.setBcc(""); + } + } + } + + private static String buildAddressString(Object[] addresses) { + if (addresses == null || addresses.length == 0) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < addresses.length; i++) { + if (i > 0) builder.append(", "); + builder.append(safeMimeDecode(addresses[i].toString())); + } + return builder.toString(); + } + + private static void processMessageContent( + Object message, + Object messageContent, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + try { + if (messageContent instanceof String stringContent) { + Method getContentType = message.getClass().getMethod("getContentType"); + String contentType = (String) getContentType.invoke(message); + + if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) { + content.setHtmlBody(stringContent); + } else { + content.setTextBody(stringContent); + } + } else { + Class multipartClass = Class.forName("jakarta.mail.Multipart"); + if (multipartClass.isInstance(messageContent)) { + processMultipart(messageContent, content, request, customHtmlSanitizer, 0); + } + } + } catch (ReflectiveOperationException | ClassCastException e) { + content.setTextBody("Email content could not be parsed with advanced processing"); + } + } + + private static void processMultipart( + Object multipart, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer, + int depth) { + + final int MAX_MULTIPART_DEPTH = 10; + if (depth > MAX_MULTIPART_DEPTH) { + content.setHtmlBody("
Maximum multipart depth exceeded
"); + return; + } + + try { + Class multipartClass = multipart.getClass(); + Method getCount = multipartClass.getMethod("getCount"); + int count = (Integer) getCount.invoke(multipart); + + Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class); + + for (int i = 0; i < count; i++) { + Object part = getBodyPart.invoke(multipart, i); + processPart(part, content, request, customHtmlSanitizer, depth + 1); + } + + } catch (ReflectiveOperationException | ClassCastException e) { + content.setHtmlBody("
Error processing multipart content
"); + } + } + + private static void processPart( + Object part, + EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer, + int depth) { + try { + Class partClass = part.getClass(); + + Method isMimeType = partClass.getMethod("isMimeType", String.class); + Method getContent = partClass.getMethod("getContent"); + Method getDisposition = partClass.getMethod("getDisposition"); + Method getFileName = partClass.getMethod("getFileName"); + Method getContentType = partClass.getMethod("getContentType"); + Method getHeader = partClass.getMethod("getHeader", String.class); + + Object disposition = getDisposition.invoke(part); + String filename = (String) getFileName.invoke(part); + String contentType = (String) getContentType.invoke(part); + + String normalizedDisposition = + disposition != null ? ((String) disposition).toLowerCase() : null; + + if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) { + Object partContent = getContent.invoke(part); + if (partContent instanceof String stringContent) { + content.setTextBody(stringContent); + } + } else if ((Boolean) isMimeType.invoke(part, TEXT_HTML) + && normalizedDisposition == null) { + Object partContent = getContent.invoke(part); + if (partContent instanceof String stringContent) { + String htmlBody = + customHtmlSanitizer != null + ? customHtmlSanitizer.sanitize(stringContent) + : stringContent; + content.setHtmlBody(htmlBody); + } + } else if ((normalizedDisposition != null + && normalizedDisposition.contains(DISPOSITION_ATTACHMENT)) + || (filename != null && !filename.trim().isEmpty())) { + + processAttachment( + part, content, request, getHeader, getContent, filename, contentType); + } else if ((Boolean) isMimeType.invoke(part, "multipart/*")) { + Object multipartContent = getContent.invoke(part); + if (multipartContent != null) { + Class multipartClass = Class.forName("jakarta.mail.Multipart"); + if (multipartClass.isInstance(multipartContent)) { + processMultipart( + multipartContent, content, request, customHtmlSanitizer, depth + 1); + } + } + } + + } catch (ReflectiveOperationException | RuntimeException e) { + // Continue processing other parts if one fails + } + } + + private static void processAttachment( + Object part, + EmailContent content, + EmlToPdfRequest request, + Method getHeader, + Method getContent, + String filename, + String contentType) { + + content.setAttachmentCount(content.getAttachmentCount() + 1); + + if (filename != null && !filename.trim().isEmpty()) { + EmailAttachment attachment = new EmailAttachment(); + attachment.setFilename(safeMimeDecode(filename)); + attachment.setContentType(contentType); + + try { + String[] contentIdHeaders = (String[]) getHeader.invoke(part, HEADER_CONTENT_ID); + if (contentIdHeaders != null) { + for (String contentIdHeader : contentIdHeaders) { + if (contentIdHeader != null && !contentIdHeader.trim().isEmpty()) { + attachment.setEmbedded(true); + String contentId = contentIdHeader.trim().replaceAll("[<>]", ""); + attachment.setContentId(contentId); + break; + } + } + } + } catch (ReflectiveOperationException e) { + } + + if ((request != null && request.isIncludeAttachments()) || attachment.isEmbedded()) { + extractAttachmentData(part, attachment, getContent, request); + } + + content.getAttachments().add(attachment); + } + } + + private static void extractAttachmentData( + Object part, EmailAttachment attachment, Method getContent, EmlToPdfRequest request) { + try { + Object attachmentContent = getContent.invoke(part); + byte[] attachmentData = null; + + if (attachmentContent instanceof InputStream inputStream) { + try (InputStream stream = inputStream) { + attachmentData = stream.readAllBytes(); + } catch (IOException e) { + if (attachment.isEmbedded()) { + attachmentData = new byte[0]; + } else { + throw new RuntimeException(e); + } + } + } else if (attachmentContent instanceof byte[] byteArray) { + attachmentData = byteArray; + } else if (attachmentContent instanceof String stringContent) { + attachmentData = stringContent.getBytes(StandardCharsets.UTF_8); + } + + if (attachmentData != null) { + long maxSizeMB = request != null ? request.getMaxAttachmentSizeMB() : 10L; + long maxSizeBytes = maxSizeMB * 1024 * 1024; + + if (attachmentData.length <= maxSizeBytes || attachment.isEmbedded()) { + attachment.setData(attachmentData); + attachment.setSizeBytes(attachmentData.length); + } else { + attachment.setSizeBytes(attachmentData.length); + } + } + } catch (ReflectiveOperationException | RuntimeException e) { + // Continue without attachment data + } + } + + private static String extractBasicHeader(String emlContent, String headerName) { + try { + String[] lines = emlContent.split("\r?\n"); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + if (line.toLowerCase().startsWith(headerName.toLowerCase())) { + StringBuilder value = + new StringBuilder(line.substring(headerName.length()).trim()); + for (int j = i + 1; j < lines.length; j++) { + if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) { + value.append(" ").append(lines[j].trim()); + } else { + break; + } + } + return safeMimeDecode(value.toString()); + } + if (line.trim().isEmpty()) break; + } + } catch (RuntimeException e) { + // Ignore errors in header extraction + } + return ""; + } + + private static String extractHtmlBody(String emlContent) { + try { + String lowerContent = emlContent.toLowerCase(); + int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML); + if (htmlStart == -1) return null; + + int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart); + if (bodyStart == -1) return null; + + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + + return emlContent.substring(bodyStart, bodyEnd).trim(); + } catch (Exception e) { + return null; + } + } + + private static String extractTextBody(String emlContent) { + try { + String lowerContent = emlContent.toLowerCase(); + int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN); + if (textStart == -1) { + int bodyStart = emlContent.indexOf("\r\n\r\n"); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n"); + if (bodyStart != -1) { + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + return emlContent.substring(bodyStart, bodyEnd).trim(); + } + return null; + } + + int bodyStart = emlContent.indexOf("\r\n\r\n", textStart); + if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", textStart); + if (bodyStart == -1) return null; + + bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; + int bodyEnd = findPartEnd(emlContent, bodyStart); + + return emlContent.substring(bodyStart, bodyEnd).trim(); + } catch (RuntimeException e) { + return null; + } + } + + private static int findPartEnd(String content, int start) { + String[] lines = content.substring(start).split("\r?\n"); + StringBuilder result = new StringBuilder(); + + for (String line : lines) { + if (line.startsWith("--") && line.length() > 10) break; + result.append(line).append("\n"); + } + + return start + result.length(); + } + + private static List extractAttachmentsBasic(String emlContent) { + List attachments = new ArrayList<>(); + try { + String[] lines = emlContent.split("\r?\n"); + boolean inHeaders = true; + String currentContentType = ""; + String currentDisposition = ""; + String currentFilename = ""; + String currentEncoding = ""; + + for (String line : lines) { + String lowerLine = line.toLowerCase().trim(); + + if (line.trim().isEmpty()) { + inHeaders = false; + if (isAttachment(currentDisposition, currentFilename, currentContentType)) { + EmailAttachment attachment = new EmailAttachment(); + attachment.setFilename(currentFilename); + attachment.setContentType(currentContentType); + attachment.setTransferEncoding(currentEncoding); + attachments.add(attachment); + } + currentContentType = ""; + currentDisposition = ""; + currentFilename = ""; + currentEncoding = ""; + inHeaders = true; + continue; + } + + if (!inHeaders) continue; + + if (lowerLine.startsWith(HEADER_CONTENT_TYPE)) { + currentContentType = line.substring(HEADER_CONTENT_TYPE.length()).trim(); + } else if (lowerLine.startsWith(HEADER_CONTENT_DISPOSITION)) { + currentDisposition = line.substring(HEADER_CONTENT_DISPOSITION.length()).trim(); + currentFilename = extractFilenameFromDisposition(currentDisposition); + } else if (lowerLine.startsWith(HEADER_CONTENT_TRANSFER_ENCODING)) { + currentEncoding = + line.substring(HEADER_CONTENT_TRANSFER_ENCODING.length()).trim(); + } + } + } catch (RuntimeException e) { + // Continue with empty list + } + return attachments; + } + + private static boolean isAttachment(String disposition, String filename, String contentType) { + return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty()) + || (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/")) + || (contentType.toLowerCase().contains("application/") && !filename.isEmpty()); + } + + private static String extractFilenameFromDisposition(String disposition) { + if (disposition == null || !disposition.contains("filename=")) { + return ""; + } + + // Handle filename*= (RFC 2231 encoded filename) + if (disposition.toLowerCase().contains("filename*=")) { + int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10; + int filenameStarEnd = disposition.indexOf(";", filenameStarStart); + if (filenameStarEnd == -1) filenameStarEnd = disposition.length(); + String extendedFilename = + disposition.substring(filenameStarStart, filenameStarEnd).trim(); + extendedFilename = extendedFilename.replaceAll("^\"|\"$", ""); + + if (extendedFilename.contains("'")) { + String[] parts = extendedFilename.split("'", 3); + if (parts.length == 3) { + return EmlProcessingUtils.decodeUrlEncoded(parts[2]); + } + } + } + + // Handle regular filename= + int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9; + int filenameEnd = disposition.indexOf(";", filenameStart); + if (filenameEnd == -1) filenameEnd = disposition.length(); + String filename = disposition.substring(filenameStart, filenameEnd).trim(); + filename = filename.replaceAll("^\"|\"$", ""); + return safeMimeDecode(filename); + } + + public static String safeMimeDecode(String headerValue) { + if (headerValue == null || headerValue.trim().isEmpty()) { + return ""; + } + + if (!mimeUtilityChecked) { + synchronized (EmlParser.class) { + if (!mimeUtilityChecked) { + initializeMimeUtilityDecoding(); + } + } + } + + if (mimeUtilityDecodeTextMethod != null) { + try { + return (String) mimeUtilityDecodeTextMethod.invoke(null, headerValue.trim()); + } catch (ReflectiveOperationException | RuntimeException e) { + // Fall through to custom implementation + } + } + + return EmlProcessingUtils.decodeMimeHeader(headerValue.trim()); + } + + private static void initializeMimeUtilityDecoding() { + try { + Class mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility"); + mimeUtilityDecodeTextMethod = mimeUtilityClass.getMethod("decodeText", String.class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + mimeUtilityDecodeTextMethod = null; + } + mimeUtilityChecked = true; + } + + @Data + public static class EmailContent { + private String subject; + private String from; + private String to; + private String cc; + private String bcc; + private Date date; + private String dateString; // For basic parsing fallback + private String htmlBody; + private String textBody; + private int attachmentCount; + private List attachments = new ArrayList<>(); + + public void setHtmlBody(String htmlBody) { + this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null; + } + + public void setTextBody(String textBody) { + this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null; + } + } + + @Data + public static class EmailAttachment { + private String filename; + private String contentType; + private byte[] data; + private boolean embedded; + private String embeddedFilename; + private long sizeBytes; + private String contentId; + private String disposition; + private String transferEncoding; + + public void setData(byte[] data) { + this.data = data; + if (data != null) { + this.sizeBytes = data.length; + } + } + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java new file mode 100644 index 000000000..9acc30c16 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/EmlProcessingUtils.java @@ -0,0 +1,601 @@ +package stirling.software.common.util; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import lombok.experimental.UtilityClass; + +import stirling.software.common.model.api.converters.EmlToPdfRequest; +import stirling.software.common.model.api.converters.HTMLToPdfRequest; + +@UtilityClass +public class EmlProcessingUtils { + + // Style constants + private static final int DEFAULT_FONT_SIZE = 12; + private static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif"; + private static final float DEFAULT_LINE_HEIGHT = 1.4f; + private static final String DEFAULT_ZOOM = "1.0"; + private static final String DEFAULT_TEXT_COLOR = "#202124"; + private static final String DEFAULT_BACKGROUND_COLOR = "#ffffff"; + private static final String DEFAULT_BORDER_COLOR = "#e8eaed"; + private static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9"; + private static final String ATTACHMENT_BORDER_COLOR = "#eeeeee"; + + private static final int EML_CHECK_LENGTH = 8192; + private static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2; + + // MIME type detection + private static final Map EXTENSION_TO_MIME_TYPE = + Map.of( + ".png", "image/png", + ".jpg", "image/jpeg", + ".jpeg", "image/jpeg", + ".gif", "image/gif", + ".bmp", "image/bmp", + ".webp", "image/webp", + ".svg", "image/svg+xml", + ".ico", "image/x-icon", + ".tiff", "image/tiff", + ".tif", "image/tiff"); + + public static void validateEmlInput(byte[] emlBytes) { + if (emlBytes == null || emlBytes.length == 0) { + throw new IllegalArgumentException("EML file is empty or null"); + } + + if (isInvalidEmlFormat(emlBytes)) { + throw new IllegalArgumentException("Invalid EML file format"); + } + } + + private static boolean isInvalidEmlFormat(byte[] emlBytes) { + try { + int checkLength = Math.min(emlBytes.length, EML_CHECK_LENGTH); + String content; + + try { + content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8); + if (content.contains("\uFFFD")) { + content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1); + } + } catch (Exception e) { + content = new String(emlBytes, 0, checkLength, StandardCharsets.ISO_8859_1); + } + + String lowerContent = content.toLowerCase(Locale.ROOT); + + boolean hasFrom = + lowerContent.contains("from:") || lowerContent.contains("return-path:"); + boolean hasSubject = lowerContent.contains("subject:"); + boolean hasMessageId = lowerContent.contains("message-id:"); + boolean hasDate = lowerContent.contains("date:"); + boolean hasTo = + lowerContent.contains("to:") + || lowerContent.contains("cc:") + || lowerContent.contains("bcc:"); + boolean hasMimeStructure = + lowerContent.contains("multipart/") + || lowerContent.contains("text/plain") + || lowerContent.contains("text/html") + || lowerContent.contains("boundary="); + + int headerCount = 0; + if (hasFrom) headerCount++; + if (hasSubject) headerCount++; + if (hasMessageId) headerCount++; + if (hasDate) headerCount++; + if (hasTo) headerCount++; + + return headerCount < MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure; + + } catch (RuntimeException e) { + return false; + } + } + + public static String generateEnhancedEmailHtml( + EmlParser.EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + StringBuilder html = new StringBuilder(); + + html.append( + String.format( + """ + + + %s + + + """); + + html.append( + String.format( + """ + \n"); + return html.toString(); + } + + public static String processEmailHtmlBody( + String htmlBody, + EmlParser.EmailContent emailContent, + CustomHtmlSanitizer customHtmlSanitizer) { + if (htmlBody == null) return ""; + + String processed = + customHtmlSanitizer != null ? customHtmlSanitizer.sanitize(htmlBody) : htmlBody; + + processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", ""); + processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", ""); + + if (emailContent != null && !emailContent.getAttachments().isEmpty()) { + processed = PdfAttachmentHandler.processInlineImages(processed, emailContent); + } + + return processed; + } + + public static String convertTextToHtml( + String textBody, CustomHtmlSanitizer customHtmlSanitizer) { + if (textBody == null) return ""; + + String html = + customHtmlSanitizer != null + ? customHtmlSanitizer.sanitize(textBody) + : escapeHtml(textBody); + + html = html.replace("\r\n", "\n").replace("\r", "\n"); + html = html.replace("\n", "
\n"); + + html = + html.replaceAll( + "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", + "$1"); + + html = + html.replaceAll( + "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})", + "$1"); + + return html; + } + + private static void appendEnhancedStyles(StringBuilder html) { + String css = + String.format( + """ + body { + font-family: %s; + font-size: %dpx; + line-height: %s; + color: %s; + margin: 0; + padding: 16px; + background-color: %s; + } + + .email-container { + width: 100%%; + max-width: 100%%; + margin: 0 auto; + } + + .email-header { + padding-bottom: 10px; + border-bottom: 1px solid %s; + margin-bottom: 10px; + } + + .email-header h1 { + margin: 0 0 10px 0; + font-size: %dpx; + font-weight: bold; + } + + .email-meta div { + margin-bottom: 2px; + font-size: %dpx; + } + + .email-body { + word-wrap: break-word; + } + + .attachment-section { + margin-top: 15px; + padding: 10px; + background-color: %s; + border: 1px solid %s; + border-radius: 3px; + } + + .attachment-section h3 { + margin: 0 0 8px 0; + font-size: %dpx; + } + + .attachment-item { + padding: 5px 0; + } + + .attachment-icon { + margin-right: 5px; + } + + .attachment-details, .attachment-type { + font-size: %dpx; + color: #555555; + } + + .attachment-inclusion-note, .attachment-info-note { + margin-top: 8px; + padding: 6px; + font-size: %dpx; + border-radius: 3px; + } + + .attachment-inclusion-note { + background-color: #e6ffed; + border: 1px solid #d4f7dc; + color: #006420; + } + + .attachment-info-note { + background-color: #fff9e6; + border: 1px solid #fff0c2; + color: #664d00; + } + + .attachment-link-container { + display: flex; + align-items: center; + padding: 8px; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 4px; + margin: 4px 0; + } + + .attachment-link-container:hover { + background-color: #e9ecef; + } + + .attachment-note { + font-size: %dpx; + color: #6c757d; + font-style: italic; + margin-left: 8px; + } + + .no-content { + padding: 20px; + text-align: center; + color: #666; + font-style: italic; + } + + .text-body { + white-space: pre-wrap; + } + + img { + max-width: 100%%; + height: auto; + display: block; + } + """, + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + DEFAULT_LINE_HEIGHT, + DEFAULT_TEXT_COLOR, + DEFAULT_BACKGROUND_COLOR, + DEFAULT_BORDER_COLOR, + DEFAULT_FONT_SIZE + 4, + DEFAULT_FONT_SIZE - 1, + ATTACHMENT_BACKGROUND_COLOR, + ATTACHMENT_BORDER_COLOR, + DEFAULT_FONT_SIZE + 1, + DEFAULT_FONT_SIZE - 2, + DEFAULT_FONT_SIZE - 2, + DEFAULT_FONT_SIZE - 3); + + html.append(css); + } + + private static void appendAttachmentsSection( + StringBuilder html, + EmlParser.EmailContent content, + EmlToPdfRequest request, + CustomHtmlSanitizer customHtmlSanitizer) { + html.append("
\n"); + int displayedAttachmentCount = + content.getAttachmentCount() > 0 + ? content.getAttachmentCount() + : content.getAttachments().size(); + html.append("

Attachments (").append(displayedAttachmentCount).append(")

\n"); + + if (!content.getAttachments().isEmpty()) { + for (int i = 0; i < content.getAttachments().size(); i++) { + EmlParser.EmailAttachment attachment = content.getAttachments().get(i); + + String embeddedFilename = + attachment.getFilename() != null + ? attachment.getFilename() + : ("attachment_" + i); + attachment.setEmbeddedFilename(embeddedFilename); + + String sizeStr = GeneralUtils.formatBytes(attachment.getSizeBytes()); + String contentType = + attachment.getContentType() != null + && !attachment.getContentType().isEmpty() + ? ", " + escapeHtml(attachment.getContentType()) + : ""; + + String attachmentId = "attachment_" + i; + html.append( + String.format( + """ +
+ @ + %s + (%s%s) +
+ """, + attachmentId, + escapeHtml(embeddedFilename), + escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())), + sizeStr, + contentType)); + } + } + + if (request != null && request.isIncludeAttachments()) { + html.append( + """ +
+

Attachments are embedded in the file.

+
+ """); + } else { + html.append( + """ +
+

Attachment information displayed - files not included in PDF.

+
+ """); + } + html.append("
\n"); + } + + public static HTMLToPdfRequest createHtmlRequest(EmlToPdfRequest request) { + HTMLToPdfRequest htmlRequest = new HTMLToPdfRequest(); + + if (request != null) { + htmlRequest.setFileInput(request.getFileInput()); + } + + htmlRequest.setZoom(Float.parseFloat(DEFAULT_ZOOM)); + return htmlRequest; + } + + public static String detectMimeType(String filename, String existingMimeType) { + if (existingMimeType != null && !existingMimeType.isEmpty()) { + return existingMimeType; + } + + if (filename != null) { + String lowerFilename = filename.toLowerCase(); + for (Map.Entry entry : EXTENSION_TO_MIME_TYPE.entrySet()) { + if (lowerFilename.endsWith(entry.getKey())) { + return entry.getValue(); + } + } + } + + return "image/png"; + } + + public static String decodeUrlEncoded(String encoded) { + try { + return java.net.URLDecoder.decode(encoded, StandardCharsets.UTF_8); + } catch (Exception e) { + return encoded; // Return original if decoding fails + } + } + + public static String decodeMimeHeader(String encodedText) { + if (encodedText == null || encodedText.trim().isEmpty()) { + return encodedText; + } + + try { + StringBuilder result = new StringBuilder(); + Pattern concatenatedPattern = + Pattern.compile( + "(=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)(\\s*=\\?[^?]+\\?[BbQq]\\?[^?]*\\?=)+"); + Matcher concatenatedMatcher = concatenatedPattern.matcher(encodedText); + String processedText = + concatenatedMatcher.replaceAll( + match -> match.group().replaceAll("\\s+(?==\\?)", "")); + + Pattern mimePattern = Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); + Matcher matcher = mimePattern.matcher(processedText); + int lastEnd = 0; + + while (matcher.find()) { + result.append(processedText, lastEnd, matcher.start()); + + String charset = matcher.group(1); + String encoding = matcher.group(2).toUpperCase(); + String encodedValue = matcher.group(3); + + try { + String decodedValue = + switch (encoding) { + case "B" -> { + String cleanBase64 = encodedValue.replaceAll("\\s", ""); + byte[] decodedBytes = Base64.getDecoder().decode(cleanBase64); + Charset targetCharset; + try { + targetCharset = Charset.forName(charset); + } catch (Exception e) { + targetCharset = StandardCharsets.UTF_8; + } + yield new String(decodedBytes, targetCharset); + } + case "Q" -> decodeQuotedPrintable(encodedValue, charset); + default -> matcher.group(0); // Return original if unknown encoding + }; + result.append(decodedValue); + } catch (RuntimeException e) { + result.append(matcher.group(0)); // Keep original on decode error + } + + lastEnd = matcher.end(); + } + + result.append(processedText.substring(lastEnd)); + return result.toString(); + } catch (Exception e) { + return encodedText; // Return original on any parsing error + } + } + + private static String decodeQuotedPrintable(String encodedText, String charset) { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < encodedText.length(); i++) { + char c = encodedText.charAt(i); + switch (c) { + case '=' -> { + if (i + 2 < encodedText.length()) { + String hex = encodedText.substring(i + 1, i + 3); + try { + int value = Integer.parseInt(hex, 16); + result.append((char) value); + i += 2; + } catch (NumberFormatException e) { + result.append(c); + } + } else if (i + 1 == encodedText.length() + || (i + 2 == encodedText.length() + && encodedText.charAt(i + 1) == '\n')) { + if (i + 1 < encodedText.length() && encodedText.charAt(i + 1) == '\n') { + i++; // Skip the newline too + } + } else { + result.append(c); + } + } + case '_' -> result.append(' '); // Space encoding in Q encoding + default -> result.append(c); + } + } + + byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1); + try { + Charset targetCharset = Charset.forName(charset); + return new String(bytes, targetCharset); + } catch (Exception e) { + try { + return new String(bytes, StandardCharsets.UTF_8); + } catch (Exception fallbackException) { + return new String(bytes, StandardCharsets.ISO_8859_1); + } + } + } + + public static String escapeHtml(String text) { + if (text == null) return ""; + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + public static String sanitizeText(String text, CustomHtmlSanitizer customHtmlSanitizer) { + if (customHtmlSanitizer != null) { + return customHtmlSanitizer.sanitize(text); + } else { + return escapeHtml(text); + } + } + + public static String simplifyHtmlContent(String htmlContent) { + String simplified = htmlContent.replaceAll("(?i)]*>.*?", ""); + simplified = simplified.replaceAll("(?i)]*>.*?", ""); + return simplified; + } +} diff --git a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java index 6b28dc683..85005af40 100644 --- a/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java +++ b/app/common/src/main/java/stirling/software/common/util/EmlToPdf.java @@ -1,131 +1,23 @@ package stirling.software.common.util; -import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.pdfbox.pdmodel.PDDocument; -import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; -import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; -import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.PageMode; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; -import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; -import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import lombok.Data; -import lombok.Getter; import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.api.converters.EmlToPdfRequest; -import stirling.software.common.model.api.converters.HTMLToPdfRequest; import stirling.software.common.service.CustomPDFDocumentFactory; -@Slf4j @UtilityClass public class EmlToPdf { - private static final class StyleConstants { - // Font and layout constants - static final int DEFAULT_FONT_SIZE = 12; - static final String DEFAULT_FONT_FAMILY = "Helvetica, sans-serif"; - static final float DEFAULT_LINE_HEIGHT = 1.4f; - static final String DEFAULT_ZOOM = "1.0"; - - // Color constants - aligned with application theme - static final String DEFAULT_TEXT_COLOR = "#202124"; - static final String DEFAULT_BACKGROUND_COLOR = "#ffffff"; - static final String DEFAULT_BORDER_COLOR = "#e8eaed"; - static final String ATTACHMENT_BACKGROUND_COLOR = "#f9f9f9"; - static final String ATTACHMENT_BORDER_COLOR = "#eeeeee"; - - // Size constants for PDF annotations - static final float ATTACHMENT_ICON_WIDTH = 12f; - static final float ATTACHMENT_ICON_HEIGHT = 14f; - static final float ANNOTATION_X_OFFSET = 2f; - static final float ANNOTATION_Y_OFFSET = 10f; - - // Content validation constants - static final int EML_CHECK_LENGTH = 8192; - static final int MIN_HEADER_COUNT_FOR_VALID_EML = 2; - - private StyleConstants() {} - } - - private static final class MimeConstants { - static final Pattern MIME_ENCODED_PATTERN = - Pattern.compile("=\\?([^?]+)\\?([BbQq])\\?([^?]*)\\?="); - static final String ATTACHMENT_MARKER = "@"; - - private MimeConstants() {} - } - - private static final class FileSizeConstants { - static final long BYTES_IN_KB = 1024L; - static final long BYTES_IN_MB = BYTES_IN_KB * 1024L; - static final long BYTES_IN_GB = BYTES_IN_MB * 1024L; - - private FileSizeConstants() {} - } - - // Cached Jakarta Mail availability check - private static Boolean jakartaMailAvailable = null; - - private static boolean isJakartaMailAvailable() { - if (jakartaMailAvailable == null) { - try { - // Check for core Jakarta Mail classes - Class.forName("jakarta.mail.internet.MimeMessage"); - Class.forName("jakarta.mail.Session"); - Class.forName("jakarta.mail.internet.MimeUtility"); - Class.forName("jakarta.mail.internet.MimePart"); - Class.forName("jakarta.mail.internet.MimeMultipart"); - Class.forName("jakarta.mail.Multipart"); - Class.forName("jakarta.mail.Part"); - - jakartaMailAvailable = true; - log.debug("Jakarta Mail libraries are available"); - } catch (ClassNotFoundException e) { - jakartaMailAvailable = false; - log.debug("Jakarta Mail libraries are not available, using basic parsing"); - } - } - return jakartaMailAvailable; - } - public static String convertEmlToHtml(byte[] emlBytes, EmlToPdfRequest request) throws IOException { - validateEmlInput(emlBytes); + EmlProcessingUtils.validateEmlInput(emlBytes); - if (isJakartaMailAvailable()) { - return convertEmlToHtmlAdvanced(emlBytes, request); - } else { - return convertEmlToHtmlBasic(emlBytes, request); - } + EmlParser.EmailContent emailContent = + EmlParser.extractEmailContent(emlBytes, request, null); + return EmlProcessingUtils.generateEnhancedEmailHtml(emailContent, request, null); } public static byte[] convertEmlToPdf( @@ -133,26 +25,21 @@ public class EmlToPdf { EmlToPdfRequest request, byte[] emlBytes, String fileName, - stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory, + CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager, CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { - validateEmlInput(emlBytes); + EmlProcessingUtils.validateEmlInput(emlBytes); try { - // Generate HTML representation - EmailContent emailContent = null; - String htmlContent; + EmlParser.EmailContent emailContent = + EmlParser.extractEmailContent(emlBytes, request, customHtmlSanitizer); - if (isJakartaMailAvailable()) { - emailContent = extractEmailContentAdvanced(emlBytes, request); - htmlContent = generateEnhancedEmailHtml(emailContent, request); - } else { - htmlContent = convertEmlToHtmlBasic(emlBytes, request); - } + String htmlContent = + EmlProcessingUtils.generateEnhancedEmailHtml( + emailContent, request, customHtmlSanitizer); - // Convert HTML to PDF byte[] pdfBytes = convertHtmlToPdf( weasyprintPath, @@ -161,35 +48,23 @@ public class EmlToPdf { tempFileManager, customHtmlSanitizer); - // Attach files if available and requested if (shouldAttachFiles(emailContent, request)) { pdfBytes = - attachFilesToPdf( + PdfAttachmentHandler.attachFilesToPdf( pdfBytes, emailContent.getAttachments(), pdfDocumentFactory); } return pdfBytes; } catch (IOException | InterruptedException e) { - log.error("Failed to convert EML to PDF for file: {}", fileName, e); throw e; } catch (Exception e) { - log.error("Unexpected error during EML to PDF conversion for file: {}", fileName, e); - throw new IOException("Conversion failed: " + e.getMessage(), e); + throw new IOException("Error converting EML to PDF", e); } } - private static void validateEmlInput(byte[] emlBytes) { - if (emlBytes == null || emlBytes.length == 0) { - throw new IllegalArgumentException("EML file is empty or null"); - } - - if (isInvalidEmlFormat(emlBytes)) { - throw new IllegalArgumentException("Invalid EML file format"); - } - } - - private static boolean shouldAttachFiles(EmailContent emailContent, EmlToPdfRequest request) { + private static boolean shouldAttachFiles( + EmlParser.EmailContent emailContent, EmlToPdfRequest request) { return emailContent != null && request != null && request.isIncludeAttachments() @@ -204,7 +79,7 @@ public class EmlToPdf { CustomHtmlSanitizer customHtmlSanitizer) throws IOException, InterruptedException { - HTMLToPdfRequest htmlRequest = createHtmlRequest(request); + var htmlRequest = EmlProcessingUtils.createHtmlRequest(request); try { return FileToPdf.convertHtmlToPdf( @@ -215,8 +90,7 @@ public class EmlToPdf { tempFileManager, customHtmlSanitizer); } catch (IOException | InterruptedException e) { - log.warn("Initial HTML to PDF conversion failed, trying with simplified HTML"); - String simplifiedHtml = simplifyHtmlContent(htmlContent); + String simplifiedHtml = EmlProcessingUtils.simplifyHtmlContent(htmlContent); return FileToPdf.convertHtmlToPdf( weasyprintPath, htmlRequest, @@ -226,1499 +100,4 @@ public class EmlToPdf { customHtmlSanitizer); } } - - private static String simplifyHtmlContent(String htmlContent) { - String simplified = htmlContent.replaceAll("(?i)]*>.*?", ""); - simplified = simplified.replaceAll("(?i)]*>.*?", ""); - return simplified; - } - - private static String generateUniqueAttachmentId(String filename) { - return "attachment_" + filename.hashCode() + "_" + System.nanoTime(); - } - - private static String convertEmlToHtmlBasic(byte[] emlBytes, EmlToPdfRequest request) { - if (emlBytes == null || emlBytes.length == 0) { - throw new IllegalArgumentException("EML file is empty or null"); - } - - String emlContent = new String(emlBytes, StandardCharsets.UTF_8); - - // Basic email parsing - String subject = extractBasicHeader(emlContent, "Subject:"); - String from = extractBasicHeader(emlContent, "From:"); - String to = extractBasicHeader(emlContent, "To:"); - String cc = extractBasicHeader(emlContent, "Cc:"); - String bcc = extractBasicHeader(emlContent, "Bcc:"); - String date = extractBasicHeader(emlContent, "Date:"); - - // Try to extract HTML content - String htmlBody = extractHtmlBody(emlContent); - if (htmlBody == null) { - String textBody = extractTextBody(emlContent); - htmlBody = - convertTextToHtml( - textBody != null ? textBody : "Email content could not be parsed"); - } - - // Generate HTML with custom styling based on request - StringBuilder html = new StringBuilder(); - html.append("\n"); - html.append("\n"); - html.append("").append(escapeHtml(subject)).append("\n"); - html.append("\n"); - html.append("\n"); - - html.append("
\n"); - html.append("
\n"); - html.append("

").append(escapeHtml(subject)).append("

\n"); - html.append("
\n"); - html.append("
From: ").append(escapeHtml(from)).append("
\n"); - html.append("
To: ").append(escapeHtml(to)).append("
\n"); - - // Include CC and BCC if present and requested - if (request != null && request.isIncludeAllRecipients()) { - if (!cc.trim().isEmpty()) { - html.append("
CC: ").append(escapeHtml(cc)).append("
\n"); - } - if (!bcc.trim().isEmpty()) { - html.append("
BCC: ") - .append(escapeHtml(bcc)) - .append("
\n"); - } - } - - if (!date.trim().isEmpty()) { - html.append("
Date: ").append(escapeHtml(date)).append("
\n"); - } - html.append("
\n"); - - html.append("
\n"); - html.append(processEmailHtmlBody(htmlBody)); - html.append("
\n"); - - // Add attachment information - always check for and display attachments - String attachmentInfo = extractAttachmentInfo(emlContent); - if (!attachmentInfo.isEmpty()) { - html.append("
\n"); - html.append("

Attachments

\n"); - html.append(attachmentInfo); - - // Add a status message about attachment inclusion - if (request != null && request.isIncludeAttachments()) { - html.append("
\n"); - html.append( - "

Note: Attachments are saved as external files and linked in this PDF. Click the links to open files externally.

\n"); - html.append("
\n"); - } else { - html.append("
\n"); - html.append( - "

Attachment information displayed - files not included in PDF. Enable 'Include attachments' to embed files.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - } - - // Show advanced features status if requested - assert request != null; - if (request.getFileInput().isEmpty()) { - html.append("
\n"); - html.append( - "

Note: Some advanced features require Jakarta Mail dependencies.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - html.append(""); - - return html.toString(); - } - - private static EmailContent extractEmailContentAdvanced( - byte[] emlBytes, EmlToPdfRequest request) { - try { - // Use Jakarta Mail for processing - Class sessionClass = Class.forName("jakarta.mail.Session"); - Class mimeMessageClass = Class.forName("jakarta.mail.internet.MimeMessage"); - - Method getDefaultInstance = - sessionClass.getMethod("getDefaultInstance", Properties.class); - Object session = getDefaultInstance.invoke(null, new Properties()); - - // Cast the session object to the proper type for the constructor - Class[] constructorArgs = new Class[] {sessionClass, InputStream.class}; - Constructor mimeMessageConstructor = - mimeMessageClass.getConstructor(constructorArgs); - Object message = - mimeMessageConstructor.newInstance(session, new ByteArrayInputStream(emlBytes)); - - return extractEmailContentAdvanced(message, request); - - } catch (ReflectiveOperationException e) { - // Create basic EmailContent from basic processing - EmailContent content = new EmailContent(); - content.setHtmlBody(convertEmlToHtmlBasic(emlBytes, request)); - return content; - } - } - - private static String convertEmlToHtmlAdvanced(byte[] emlBytes, EmlToPdfRequest request) { - EmailContent content = extractEmailContentAdvanced(emlBytes, request); - return generateEnhancedEmailHtml(content, request); - } - - private static String extractAttachmentInfo(String emlContent) { - StringBuilder attachmentInfo = new StringBuilder(); - try { - String[] lines = emlContent.split("\r?\n"); - boolean inHeaders = true; - String currentContentType = ""; - String currentDisposition = ""; - String currentFilename = ""; - String currentEncoding = ""; - boolean inMultipart = false; - String boundary = ""; - - // First pass: find boundary for multipart messages - for (String line : lines) { - String lowerLine = line.toLowerCase().trim(); - if (lowerLine.startsWith("content-type:") && lowerLine.contains("multipart")) { - if (lowerLine.contains("boundary=")) { - int boundaryStart = lowerLine.indexOf("boundary=") + 9; - String boundaryPart = line.substring(boundaryStart).trim(); - if (boundaryPart.startsWith("\"")) { - boundary = boundaryPart.substring(1, boundaryPart.indexOf("\"", 1)); - } else { - int spaceIndex = boundaryPart.indexOf(" "); - boundary = - spaceIndex > 0 - ? boundaryPart.substring(0, spaceIndex) - : boundaryPart; - } - inMultipart = true; - break; - } - } - if (line.trim().isEmpty()) break; - } - - // Second pass: extract attachment information - for (String line : lines) { - String lowerLine = line.toLowerCase().trim(); - - // Check for boundary markers in multipart messages - if (inMultipart && line.trim().startsWith("--" + boundary)) { - // Reset for new part - currentContentType = ""; - currentDisposition = ""; - currentFilename = ""; - currentEncoding = ""; - inHeaders = true; - continue; - } - - if (inHeaders && line.trim().isEmpty()) { - inHeaders = false; - - // Process accumulated attachment info - if (isAttachment(currentDisposition, currentFilename, currentContentType)) { - addAttachmentToInfo( - attachmentInfo, - currentFilename, - currentContentType, - currentEncoding); - - // Reset for next attachment - currentContentType = ""; - currentDisposition = ""; - currentFilename = ""; - currentEncoding = ""; - } - continue; - } - - if (!inHeaders) continue; // Skip body content - - // Parse headers - if (lowerLine.startsWith("content-type:")) { - currentContentType = line.substring(13).trim(); - } else if (lowerLine.startsWith("content-disposition:")) { - currentDisposition = line.substring(20).trim(); - // Extract filename if present - currentFilename = extractFilenameFromDisposition(currentDisposition); - } else if (lowerLine.startsWith("content-transfer-encoding:")) { - currentEncoding = line.substring(26).trim(); - } else if (line.startsWith(" ") || line.startsWith("\t")) { - // Continuation of previous header - if (currentDisposition.contains("filename=")) { - currentDisposition += " " + line.trim(); - currentFilename = extractFilenameFromDisposition(currentDisposition); - } else if (!currentContentType.isEmpty()) { - currentContentType += " " + line.trim(); - } - } - } - - if (isAttachment(currentDisposition, currentFilename, currentContentType)) { - addAttachmentToInfo( - attachmentInfo, currentFilename, currentContentType, currentEncoding); - } - - } catch (RuntimeException e) { - log.warn("Error extracting attachment info: {}", e.getMessage()); - } - return attachmentInfo.toString(); - } - - private static boolean isAttachment(String disposition, String filename, String contentType) { - return (disposition.toLowerCase().contains("attachment") && !filename.isEmpty()) - || (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/")) - || (contentType.toLowerCase().contains("application/") && !filename.isEmpty()); - } - - private static String extractFilenameFromDisposition(String disposition) { - if (disposition.contains("filename=")) { - int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9; - int filenameEnd = disposition.indexOf(";", filenameStart); - if (filenameEnd == -1) filenameEnd = disposition.length(); - String filename = disposition.substring(filenameStart, filenameEnd).trim(); - filename = filename.replaceAll("^\"|\"$", ""); - // Apply MIME decoding to handle encoded filenames - return safeMimeDecode(filename); - } - return ""; - } - - private static void addAttachmentToInfo( - StringBuilder attachmentInfo, String filename, String contentType, String encoding) { - // Create attachment info with paperclip emoji before filename - attachmentInfo - .append("
") - .append("") - .append(MimeConstants.ATTACHMENT_MARKER) - .append(" ") - .append("") - .append(escapeHtml(filename)) - .append(""); - - // Add content type and encoding info - if (!contentType.isEmpty() || !encoding.isEmpty()) { - attachmentInfo.append(" ("); - if (!contentType.isEmpty()) { - attachmentInfo.append(escapeHtml(contentType)); - } - if (!encoding.isEmpty()) { - if (!contentType.isEmpty()) attachmentInfo.append(", "); - attachmentInfo.append("encoding: ").append(escapeHtml(encoding)); - } - attachmentInfo.append(")"); - } - attachmentInfo.append("
\n"); - } - - private static boolean isInvalidEmlFormat(byte[] emlBytes) { - try { - int checkLength = Math.min(emlBytes.length, StyleConstants.EML_CHECK_LENGTH); - String content = new String(emlBytes, 0, checkLength, StandardCharsets.UTF_8); - String lowerContent = content.toLowerCase(); - - boolean hasFrom = - lowerContent.contains("from:") || lowerContent.contains("return-path:"); - boolean hasSubject = lowerContent.contains("subject:"); - boolean hasMessageId = lowerContent.contains("message-id:"); - boolean hasDate = lowerContent.contains("date:"); - boolean hasTo = - lowerContent.contains("to:") - || lowerContent.contains("cc:") - || lowerContent.contains("bcc:"); - boolean hasMimeStructure = - lowerContent.contains("multipart/") - || lowerContent.contains("text/plain") - || lowerContent.contains("text/html") - || lowerContent.contains("boundary="); - - int headerCount = 0; - if (hasFrom) headerCount++; - if (hasSubject) headerCount++; - if (hasMessageId) headerCount++; - if (hasDate) headerCount++; - if (hasTo) headerCount++; - - return headerCount < StyleConstants.MIN_HEADER_COUNT_FOR_VALID_EML && !hasMimeStructure; - - } catch (RuntimeException e) { - return false; - } - } - - private static String extractBasicHeader(String emlContent, String headerName) { - try { - String[] lines = emlContent.split("\r?\n"); - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; - if (line.toLowerCase().startsWith(headerName.toLowerCase())) { - StringBuilder value = - new StringBuilder(line.substring(headerName.length()).trim()); - // Handle multi-line headers - for (int j = i + 1; j < lines.length; j++) { - if (lines[j].startsWith(" ") || lines[j].startsWith("\t")) { - value.append(" ").append(lines[j].trim()); - } else { - break; - } - } - // Apply MIME header decoding - return safeMimeDecode(value.toString()); - } - if (line.trim().isEmpty()) break; - } - } catch (RuntimeException e) { - log.warn("Error extracting header '{}': {}", headerName, e.getMessage()); - } - return ""; - } - - private static String extractHtmlBody(String emlContent) { - try { - String lowerContent = emlContent.toLowerCase(); - int htmlStart = lowerContent.indexOf("content-type: text/html"); - if (htmlStart == -1) return null; - - return getString(emlContent, htmlStart); - - } catch (Exception e) { - return null; - } - } - - @Nullable - private static String getString(String emlContent, int htmlStart) { - int bodyStart = emlContent.indexOf("\r\n\r\n", htmlStart); - if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n", htmlStart); - if (bodyStart == -1) return null; - - bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; - int bodyEnd = findPartEnd(emlContent, bodyStart); - - return emlContent.substring(bodyStart, bodyEnd).trim(); - } - - private static String extractTextBody(String emlContent) { - try { - String lowerContent = emlContent.toLowerCase(); - int textStart = lowerContent.indexOf("content-type: text/plain"); - if (textStart == -1) { - int bodyStart = emlContent.indexOf("\r\n\r\n"); - if (bodyStart == -1) bodyStart = emlContent.indexOf("\n\n"); - if (bodyStart != -1) { - bodyStart += (emlContent.charAt(bodyStart + 1) == '\r') ? 4 : 2; - int bodyEnd = findPartEnd(emlContent, bodyStart); - return emlContent.substring(bodyStart, bodyEnd).trim(); - } - return null; - } - - return getString(emlContent, textStart); - - } catch (RuntimeException e) { - return null; - } - } - - private static int findPartEnd(String content, int start) { - String[] lines = content.substring(start).split("\r?\n"); - StringBuilder result = new StringBuilder(); - - for (String line : lines) { - if (line.startsWith("--") && line.length() > 10) break; - result.append(line).append("\n"); - } - - return start + result.length(); - } - - private static String convertTextToHtml(String textBody) { - if (textBody == null) return ""; - - String html = escapeHtml(textBody); - html = html.replace("\r\n", "\n").replace("\r", "\n"); - html = html.replace("\n", "
\n"); - - html = - html.replaceAll( - "(https?://[\\w\\-._~:/?#\\[\\]@!$&'()*+,;=%]+)", - "$1"); - - html = - html.replaceAll( - "([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,63})", - "$1"); - - return html; - } - - private static String processEmailHtmlBody(String htmlBody) { - return processEmailHtmlBody(htmlBody, null); - } - - private static String processEmailHtmlBody(String htmlBody, EmailContent emailContent) { - if (htmlBody == null) return ""; - - String processed = htmlBody; - - // Remove problematic CSS - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*fixed[^;]*;?", ""); - processed = processed.replaceAll("(?i)\\s*position\\s*:\\s*absolute[^;]*;?", ""); - - // Process inline images (cid: references) if we have email content with attachments - if (emailContent != null && !emailContent.getAttachments().isEmpty()) { - processed = processInlineImages(processed, emailContent); - } - - return processed; - } - - private static String processInlineImages(String htmlContent, EmailContent emailContent) { - if (htmlContent == null || emailContent == null) return htmlContent; - - // Create a map of Content-ID to attachment data - Map contentIdMap = new HashMap<>(); - for (EmailAttachment attachment : emailContent.getAttachments()) { - if (attachment.isEmbedded() - && attachment.getContentId() != null - && attachment.getData() != null) { - contentIdMap.put(attachment.getContentId(), attachment); - } - } - - if (contentIdMap.isEmpty()) return htmlContent; - - // Pattern to match cid: references in img src attributes - Pattern cidPattern = - Pattern.compile( - "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", - Pattern.CASE_INSENSITIVE); - Matcher matcher = cidPattern.matcher(htmlContent); - - StringBuffer result = new StringBuffer(); - while (matcher.find()) { - String contentId = matcher.group(1); - EmailAttachment attachment = contentIdMap.get(contentId); - - if (attachment != null && attachment.getData() != null) { - // Convert to data URI - String mimeType = attachment.getContentType(); - if (mimeType == null || mimeType.isEmpty()) { - // Try to determine MIME type from filename - String filename = attachment.getFilename(); - if (filename != null) { - if (filename.toLowerCase().endsWith(".png")) { - mimeType = "image/png"; - } else if (filename.toLowerCase().endsWith(".jpg") - || filename.toLowerCase().endsWith(".jpeg")) { - mimeType = "image/jpeg"; - } else if (filename.toLowerCase().endsWith(".gif")) { - mimeType = "image/gif"; - } else if (filename.toLowerCase().endsWith(".bmp")) { - mimeType = "image/bmp"; - } else { - mimeType = "image/png"; // fallback - } - } else { - mimeType = "image/png"; // fallback - } - } - - String base64Data = Base64.getEncoder().encodeToString(attachment.getData()); - String dataUri = "data:" + mimeType + ";base64," + base64Data; - - // Replace the cid: reference with the data URI - String replacement = - matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri); - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } else { - // Keep original if attachment not found - matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0))); - } - } - matcher.appendTail(result); - - return result.toString(); - } - - private static void appendEnhancedStyles(StringBuilder html) { - int fontSize = StyleConstants.DEFAULT_FONT_SIZE; - String textColor = StyleConstants.DEFAULT_TEXT_COLOR; - String backgroundColor = StyleConstants.DEFAULT_BACKGROUND_COLOR; - String borderColor = StyleConstants.DEFAULT_BORDER_COLOR; - - html.append("body {\n"); - html.append(" font-family: ").append(StyleConstants.DEFAULT_FONT_FAMILY).append(";\n"); - html.append(" font-size: ").append(fontSize).append("px;\n"); - html.append(" line-height: ").append(StyleConstants.DEFAULT_LINE_HEIGHT).append(";\n"); - html.append(" color: ").append(textColor).append(";\n"); - html.append(" margin: 0;\n"); - html.append(" padding: 16px;\n"); - html.append(" background-color: ").append(backgroundColor).append(";\n"); - html.append("}\n\n"); - - html.append(".email-container {\n"); - html.append(" width: 100%;\n"); - html.append(" max-width: 100%;\n"); - html.append(" margin: 0 auto;\n"); - html.append("}\n\n"); - - html.append(".email-header {\n"); - html.append(" padding-bottom: 10px;\n"); - html.append(" border-bottom: 1px solid ").append(borderColor).append(";\n"); - html.append(" margin-bottom: 10px;\n"); - html.append("}\n\n"); - html.append(".email-header h1 {\n"); - html.append(" margin: 0 0 10px 0;\n"); - html.append(" font-size: ").append(fontSize + 4).append("px;\n"); - html.append(" font-weight: bold;\n"); - html.append("}\n\n"); - html.append(".email-meta div {\n"); - html.append(" margin-bottom: 2px;\n"); - html.append(" font-size: ").append(fontSize - 1).append("px;\n"); - html.append("}\n\n"); - - html.append(".email-body {\n"); - html.append(" word-wrap: break-word;\n"); - html.append("}\n\n"); - - html.append(".attachment-section {\n"); - html.append(" margin-top: 15px;\n"); - html.append(" padding: 10px;\n"); - html.append(" background-color: ") - .append(StyleConstants.ATTACHMENT_BACKGROUND_COLOR) - .append(";\n"); - html.append(" border: 1px solid ") - .append(StyleConstants.ATTACHMENT_BORDER_COLOR) - .append(";\n"); - html.append(" border-radius: 3px;\n"); - html.append("}\n\n"); - html.append(".attachment-section h3 {\n"); - html.append(" margin: 0 0 8px 0;\n"); - html.append(" font-size: ").append(fontSize + 1).append("px;\n"); - html.append("}\n\n"); - html.append(".attachment-item {\n"); - html.append(" padding: 5px 0;\n"); - html.append("}\n\n"); - html.append(".attachment-icon {\n"); - html.append(" margin-right: 5px;\n"); - html.append("}\n\n"); - html.append(".attachment-details, .attachment-type {\n"); - html.append(" font-size: ").append(fontSize - 2).append("px;\n"); - html.append(" color: #555555;\n"); - html.append("}\n\n"); - html.append(".attachment-inclusion-note, .attachment-info-note {\n"); - html.append(" margin-top: 8px;\n"); - html.append(" padding: 6px;\n"); - html.append(" font-size: ").append(fontSize - 2).append("px;\n"); - html.append(" border-radius: 3px;\n"); - html.append("}\n\n"); - html.append(".attachment-inclusion-note {\n"); - html.append(" background-color: #e6ffed;\n"); - html.append(" border: 1px solid #d4f7dc;\n"); - html.append(" color: #006420;\n"); - html.append("}\n\n"); - html.append(".attachment-info-note {\n"); - html.append(" background-color: #fff9e6;\n"); - html.append(" border: 1px solid #fff0c2;\n"); - html.append(" color: #664d00;\n"); - html.append("}\n\n"); - html.append(".attachment-link-container {\n"); - html.append(" display: flex;\n"); - html.append(" align-items: center;\n"); - html.append(" padding: 8px;\n"); - html.append(" background-color: #f8f9fa;\n"); - html.append(" border: 1px solid #dee2e6;\n"); - html.append(" border-radius: 4px;\n"); - html.append(" margin: 4px 0;\n"); - html.append("}\n\n"); - html.append(".attachment-link-container:hover {\n"); - html.append(" background-color: #e9ecef;\n"); - html.append("}\n\n"); - html.append(".attachment-note {\n"); - html.append(" font-size: ").append(fontSize - 3).append("px;\n"); - html.append(" color: #6c757d;\n"); - html.append(" font-style: italic;\n"); - html.append(" margin-left: 8px;\n"); - html.append("}\n\n"); - - // Basic image styling: ensure images are responsive but not overly constrained. - html.append("img {\n"); - html.append(" max-width: 100%;\n"); // Make images responsive to container width - html.append(" height: auto;\n"); // Maintain aspect ratio - html.append(" display: block;\n"); // Avoid extra space below images - html.append("}\n\n"); - } - - private static String escapeHtml(String text) { - if (text == null) return ""; - return text.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace("\"", """) - .replace("'", "'"); - } - - private static stirling.software.common.model.api.converters.HTMLToPdfRequest createHtmlRequest( - EmlToPdfRequest request) { - stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest = - new stirling.software.common.model.api.converters.HTMLToPdfRequest(); - - if (request != null) { - htmlRequest.setFileInput(request.getFileInput()); - } - - // Set default zoom level - htmlRequest.setZoom(Float.parseFloat(StyleConstants.DEFAULT_ZOOM)); - - return htmlRequest; - } - - private static EmailContent extractEmailContentAdvanced( - Object message, EmlToPdfRequest request) { - EmailContent content = new EmailContent(); - - try { - Class messageClass = message.getClass(); - - // Extract headers via reflection - Method getSubject = messageClass.getMethod("getSubject"); - String subject = (String) getSubject.invoke(message); - content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject"); - - Method getFrom = messageClass.getMethod("getFrom"); - Object[] fromAddresses = (Object[]) getFrom.invoke(message); - content.setFrom( - fromAddresses != null && fromAddresses.length > 0 - ? safeMimeDecode(fromAddresses[0].toString()) - : ""); - - Method getAllRecipients = messageClass.getMethod("getAllRecipients"); - Object[] recipients = (Object[]) getAllRecipients.invoke(message); - content.setTo( - recipients != null && recipients.length > 0 - ? safeMimeDecode(recipients[0].toString()) - : ""); - - Method getSentDate = messageClass.getMethod("getSentDate"); - content.setDate((Date) getSentDate.invoke(message)); - - // Extract content - Method getContent = messageClass.getMethod("getContent"); - Object messageContent = getContent.invoke(message); - - if (messageContent instanceof String stringContent) { - Method getContentType = messageClass.getMethod("getContentType"); - String contentType = (String) getContentType.invoke(message); - if (contentType != null && contentType.toLowerCase().contains("text/html")) { - content.setHtmlBody(stringContent); - } else { - content.setTextBody(stringContent); - } - } else { - // Handle multipart content - try { - Class multipartClass = Class.forName("jakarta.mail.Multipart"); - if (multipartClass.isInstance(messageContent)) { - processMultipartAdvanced(messageContent, content, request); - } - } catch (Exception e) { - log.warn("Error processing content: {}", e.getMessage()); - } - } - - } catch (Exception e) { - content.setSubject("Email Conversion"); - content.setFrom("Unknown"); - content.setTo("Unknown"); - content.setTextBody("Email content could not be parsed with advanced processing"); - } - - return content; - } - - private static void processMultipartAdvanced( - Object multipart, EmailContent content, EmlToPdfRequest request) { - try { - // Enhanced multipart type checking - if (!isValidJakartaMailMultipart(multipart)) { - log.warn("Invalid Jakarta Mail multipart type: {}", multipart.getClass().getName()); - return; - } - - Class multipartClass = multipart.getClass(); - Method getCount = multipartClass.getMethod("getCount"); - int count = (Integer) getCount.invoke(multipart); - - Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class); - - for (int i = 0; i < count; i++) { - Object part = getBodyPart.invoke(multipart, i); - processPartAdvanced(part, content, request); - } - - } catch (Exception e) { - content.setTextBody("Email content could not be parsed with advanced processing"); - } - } - - private static void processPartAdvanced( - Object part, EmailContent content, EmlToPdfRequest request) { - try { - if (!isValidJakartaMailPart(part)) { - log.warn("Invalid Jakarta Mail part type: {}", part.getClass().getName()); - return; - } - - Class partClass = part.getClass(); - Method isMimeType = partClass.getMethod("isMimeType", String.class); - Method getContent = partClass.getMethod("getContent"); - Method getDisposition = partClass.getMethod("getDisposition"); - Method getFileName = partClass.getMethod("getFileName"); - Method getContentType = partClass.getMethod("getContentType"); - Method getHeader = partClass.getMethod("getHeader", String.class); - - Object disposition = getDisposition.invoke(part); - String filename = (String) getFileName.invoke(part); - String contentType = (String) getContentType.invoke(part); - - if ((Boolean) isMimeType.invoke(part, "text/plain") && disposition == null) { - content.setTextBody((String) getContent.invoke(part)); - } else if ((Boolean) isMimeType.invoke(part, "text/html") && disposition == null) { - content.setHtmlBody((String) getContent.invoke(part)); - } else if ("attachment".equalsIgnoreCase((String) disposition) - || (filename != null && !filename.trim().isEmpty())) { - - content.setAttachmentCount(content.getAttachmentCount() + 1); - - // Always extract basic attachment metadata for display - if (filename != null && !filename.trim().isEmpty()) { - // Create attachment with metadata only - EmailAttachment attachment = new EmailAttachment(); - // Apply MIME decoding to filename to handle encoded attachment names - attachment.setFilename(safeMimeDecode(filename)); - attachment.setContentType(contentType); - - // Check if it's an embedded image - String[] contentIdHeaders = (String[]) getHeader.invoke(part, "Content-ID"); - if (contentIdHeaders != null && contentIdHeaders.length > 0) { - attachment.setEmbedded(true); - // Store the Content-ID, removing angle brackets if present - String contentId = contentIdHeaders[0]; - if (contentId.startsWith("<") && contentId.endsWith(">")) { - contentId = contentId.substring(1, contentId.length() - 1); - } - attachment.setContentId(contentId); - } - - // Extract attachment data if attachments should be included OR if it's an - // embedded image (needed for inline display) - if ((request != null && request.isIncludeAttachments()) - || attachment.isEmbedded()) { - try { - Object attachmentContent = getContent.invoke(part); - byte[] attachmentData = null; - - if (attachmentContent instanceof java.io.InputStream inputStream) { - try { - attachmentData = inputStream.readAllBytes(); - } catch (IOException e) { - log.warn( - "Failed to read InputStream attachment: {}", - e.getMessage()); - } - } else if (attachmentContent instanceof byte[] byteArray) { - attachmentData = byteArray; - } else if (attachmentContent instanceof String stringContent) { - attachmentData = stringContent.getBytes(StandardCharsets.UTF_8); - } - - if (attachmentData != null) { - // Check size limit (use default 10MB if request is null) - long maxSizeMB = - request != null ? request.getMaxAttachmentSizeMB() : 10L; - long maxSizeBytes = maxSizeMB * 1024 * 1024; - - if (attachmentData.length <= maxSizeBytes) { - attachment.setData(attachmentData); - attachment.setSizeBytes(attachmentData.length); - } else { - // For embedded images, always include data regardless of size - // to ensure inline display works - if (attachment.isEmbedded()) { - attachment.setData(attachmentData); - attachment.setSizeBytes(attachmentData.length); - } else { - // Still show attachment info even if too large - attachment.setSizeBytes(attachmentData.length); - } - } - } - } catch (Exception e) { - log.warn("Error extracting attachment data: {}", e.getMessage()); - } - } - - // Add attachment to the list for display (with or without data) - content.getAttachments().add(attachment); - } - } else if ((Boolean) isMimeType.invoke(part, "multipart/*")) { - // Handle nested multipart content - try { - Object multipartContent = getContent.invoke(part); - Class multipartClass = Class.forName("jakarta.mail.Multipart"); - if (multipartClass.isInstance(multipartContent)) { - processMultipartAdvanced(multipartContent, content, request); - } - } catch (Exception e) { - log.warn("Error processing multipart content: {}", e.getMessage()); - } - } - - } catch (Exception e) { - log.warn("Error processing multipart part: {}", e.getMessage()); - } - } - - private static String generateEnhancedEmailHtml(EmailContent content, EmlToPdfRequest request) { - StringBuilder html = new StringBuilder(); - - html.append("\n"); - html.append("\n"); - html.append("").append(escapeHtml(content.getSubject())).append("\n"); - html.append("\n"); - html.append("\n"); - - html.append("
\n"); - html.append("
\n"); - html.append("

").append(escapeHtml(content.getSubject())).append("

\n"); - html.append("
\n"); - html.append("
From: ") - .append(escapeHtml(content.getFrom())) - .append("
\n"); - html.append("
To: ") - .append(escapeHtml(content.getTo())) - .append("
\n"); - - if (content.getDate() != null) { - html.append("
Date: ") - .append(formatEmailDate(content.getDate())) - .append("
\n"); - } - html.append("
\n"); - - html.append("
\n"); - if (content.getHtmlBody() != null && !content.getHtmlBody().trim().isEmpty()) { - html.append(processEmailHtmlBody(content.getHtmlBody(), content)); - } else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) { - html.append("
"); - html.append(convertTextToHtml(content.getTextBody())); - html.append("
"); - } else { - html.append("
"); - html.append("

No content available

"); - html.append("
"); - } - html.append("
\n"); - - if (content.getAttachmentCount() > 0 || !content.getAttachments().isEmpty()) { - html.append("
\n"); - int displayedAttachmentCount = - content.getAttachmentCount() > 0 - ? content.getAttachmentCount() - : content.getAttachments().size(); - html.append("

Attachments (").append(displayedAttachmentCount).append(")

\n"); - - if (!content.getAttachments().isEmpty()) { - for (EmailAttachment attachment : content.getAttachments()) { - // Create attachment info with paperclip emoji before filename - String uniqueId = generateUniqueAttachmentId(attachment.getFilename()); - attachment.setEmbeddedFilename( - attachment.getEmbeddedFilename() != null - ? attachment.getEmbeddedFilename() - : attachment.getFilename()); - - html.append("
") - .append("") - .append(MimeConstants.ATTACHMENT_MARKER) - .append(" ") - .append("") - .append(escapeHtml(safeMimeDecode(attachment.getFilename()))) - .append(""); - - String sizeStr = formatFileSize(attachment.getSizeBytes()); - html.append(" (").append(sizeStr); - if (attachment.getContentType() != null - && !attachment.getContentType().isEmpty()) { - html.append(", ").append(escapeHtml(attachment.getContentType())); - } - html.append(")
\n"); - } - } - - if (request.isIncludeAttachments()) { - html.append("
\n"); - html.append("

Attachments are embedded in the file.

\n"); - html.append("
\n"); - } else { - html.append("
\n"); - html.append( - "

Attachment information displayed - files not included in PDF.

\n"); - html.append("
\n"); - } - - html.append("
\n"); - } - - html.append("
\n"); - html.append(""); - - return html.toString(); - } - - private static byte[] attachFilesToPdf( - byte[] pdfBytes, - List attachments, - CustomPDFDocumentFactory pdfDocumentFactory) - throws IOException { - try (PDDocument document = pdfDocumentFactory.load(pdfBytes); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - - if (attachments == null || attachments.isEmpty()) { - document.save(outputStream); - return outputStream.toByteArray(); - } - - List embeddedFiles = new ArrayList<>(); - - // Set up the embedded files name tree once - if (document.getDocumentCatalog().getNames() == null) { - document.getDocumentCatalog() - .setNames(new PDDocumentNameDictionary(document.getDocumentCatalog())); - } - - PDDocumentNameDictionary names = document.getDocumentCatalog().getNames(); - if (names.getEmbeddedFiles() == null) { - names.setEmbeddedFiles(new PDEmbeddedFilesNameTreeNode()); - } - - PDEmbeddedFilesNameTreeNode efTree = names.getEmbeddedFiles(); - Map efMap = efTree.getNames(); - if (efMap == null) { - efMap = new HashMap<>(); - } - - // Embed each attachment directly into the PDF - for (EmailAttachment attachment : attachments) { - if (attachment.getData() == null || attachment.getData().length == 0) { - continue; - } - - try { - // Generate unique filename - String filename = attachment.getFilename(); - if (filename == null || filename.trim().isEmpty()) { - filename = "attachment_" + System.currentTimeMillis(); - if (attachment.getContentType() != null - && attachment.getContentType().contains("/")) { - String[] parts = attachment.getContentType().split("/"); - if (parts.length > 1) { - filename += "." + parts[1]; - } - } - } - - // Ensure unique filename - String uniqueFilename = getUniqueFilename(filename, embeddedFiles, efMap); - - // Create embedded file - PDEmbeddedFile embeddedFile = - new PDEmbeddedFile( - document, new ByteArrayInputStream(attachment.getData())); - embeddedFile.setSize(attachment.getData().length); - embeddedFile.setCreationDate(new GregorianCalendar()); - - // Create file specification - PDComplexFileSpecification fileSpec = new PDComplexFileSpecification(); - fileSpec.setFile(uniqueFilename); - fileSpec.setEmbeddedFile(embeddedFile); - if (attachment.getContentType() != null) { - embeddedFile.setSubtype(attachment.getContentType()); - fileSpec.setFileDescription("Email attachment: " + uniqueFilename); - } - - // Add to the map (but don't set it yet) - efMap.put(uniqueFilename, fileSpec); - embeddedFiles.add(uniqueFilename); - - // Store the filename for annotation creation - attachment.setEmbeddedFilename(uniqueFilename); - - } catch (Exception e) { - // Log error but continue with other attachments - log.warn("Failed to embed attachment: {}", attachment.getFilename(), e); - } - } - - // Set the complete map once at the end - if (!efMap.isEmpty()) { - efTree.setNames(efMap); - - // Set catalog viewer preferences to automatically show attachments pane - setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS); - } - - // Add attachment annotations to the first page for each embedded file - if (!embeddedFiles.isEmpty()) { - addAttachmentAnnotationsToDocument(document, attachments); - } - - document.save(outputStream); - return outputStream.toByteArray(); - } - } - - private static String getUniqueFilename( - String filename, - List embeddedFiles, - Map efMap) { - String uniqueFilename = filename; - int counter = 1; - while (embeddedFiles.contains(uniqueFilename) || efMap.containsKey(uniqueFilename)) { - String extension = ""; - String baseName = filename; - int lastDot = filename.lastIndexOf('.'); - if (lastDot > 0) { - extension = filename.substring(lastDot); - baseName = filename.substring(0, lastDot); - } - uniqueFilename = baseName + "_" + counter + extension; - counter++; - } - return uniqueFilename; - } - - private static void addAttachmentAnnotationsToDocument( - PDDocument document, List attachments) throws IOException { - if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) { - return; - } - - // 1. Find the screen position of all attachment markers - AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder(); - finder.setSortByPosition(true); // Process pages in order - finder.getText(document); - List markerPositions = finder.getPositions(); - - // 2. Warn if the number of markers and attachments don't match - if (markerPositions.size() != attachments.size()) { - log.warn( - "Found {} attachment markers, but there are {} attachments. Annotation count may be incorrect.", - markerPositions.size(), - attachments.size()); - } - - // 3. Create an invisible annotation over each found marker - int annotationsToAdd = Math.min(markerPositions.size(), attachments.size()); - for (int i = 0; i < annotationsToAdd; i++) { - MarkerPosition position = markerPositions.get(i); - EmailAttachment attachment = attachments.get(i); - - if (attachment.getEmbeddedFilename() != null) { - PDPage page = document.getPage(position.getPageIndex()); - addAttachmentAnnotationToPage( - document, page, attachment, position.getX(), position.getY()); - } - } - } - - private static void addAttachmentAnnotationToPage( - PDDocument document, PDPage page, EmailAttachment attachment, float x, float y) - throws IOException { - - PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment(); - - PDRectangle rect = getPdRectangle(page, x, y); - fileAnnotation.setRectangle(rect); - - // Remove visual appearance while keeping clickable functionality - try { - PDAppearanceDictionary appearance = new PDAppearanceDictionary(); - PDAppearanceStream normalAppearance = new PDAppearanceStream(document); - normalAppearance.setBBox(new PDRectangle(0, 0, 0, 0)); // Zero-size bounding box - - appearance.setNormalAppearance(normalAppearance); - fileAnnotation.setAppearance(appearance); - } catch (Exception e) { - // If appearance manipulation fails, just set it to null - fileAnnotation.setAppearance(null); - } - - // Set invisibility flags but keep it functional - fileAnnotation.setInvisible(true); - fileAnnotation.setHidden(false); // Must be false to remain clickable - fileAnnotation.setNoView(false); // Must be false to remain clickable - fileAnnotation.setPrinted(false); - - PDEmbeddedFilesNameTreeNode efTree = - document.getDocumentCatalog().getNames().getEmbeddedFiles(); - if (efTree != null) { - Map efMap = efTree.getNames(); - if (efMap != null) { - PDComplexFileSpecification fileSpec = efMap.get(attachment.getEmbeddedFilename()); - if (fileSpec != null) { - fileAnnotation.setFile(fileSpec); - } - } - } - - fileAnnotation.setContents("Click to open: " + attachment.getFilename()); - fileAnnotation.setAnnotationName("EmbeddedFile_" + attachment.getEmbeddedFilename()); - - page.getAnnotations().add(fileAnnotation); - - log.info( - "Added attachment annotation for '{}' on page {}", - attachment.getFilename(), - document.getPages().indexOf(page) + 1); - } - - private static @NotNull PDRectangle getPdRectangle(PDPage page, float x, float y) { - PDRectangle mediaBox = page.getMediaBox(); - float pdfY = mediaBox.getHeight() - y; - - float iconWidth = - StyleConstants.ATTACHMENT_ICON_WIDTH; // Keep original size for clickability - float iconHeight = - StyleConstants.ATTACHMENT_ICON_HEIGHT; // Keep original size for clickability - - // Keep the full-size rectangle so it remains clickable - return new PDRectangle( - x + StyleConstants.ANNOTATION_X_OFFSET, - pdfY - iconHeight + StyleConstants.ANNOTATION_Y_OFFSET, - iconWidth, - iconHeight); - } - - private static String formatEmailDate(Date date) { - if (date == null) return ""; - java.text.SimpleDateFormat formatter = - new java.text.SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a", Locale.ENGLISH); - return formatter.format(date); - } - - private static String formatFileSize(long bytes) { - if (bytes < FileSizeConstants.BYTES_IN_KB) { - return bytes + " B"; - } else if (bytes < FileSizeConstants.BYTES_IN_MB) { - return String.format("%.1f KB", bytes / (double) FileSizeConstants.BYTES_IN_KB); - } else if (bytes < FileSizeConstants.BYTES_IN_GB) { - return String.format("%.1f MB", bytes / (double) FileSizeConstants.BYTES_IN_MB); - } else { - return String.format("%.1f GB", bytes / (double) FileSizeConstants.BYTES_IN_GB); - } - } - - // MIME header decoding functionality for RFC 2047 encoded headers - moved to constants - - private static String decodeMimeHeader(String encodedText) { - if (encodedText == null || encodedText.trim().isEmpty()) { - return encodedText; - } - - try { - StringBuilder result = new StringBuilder(); - Matcher matcher = MimeConstants.MIME_ENCODED_PATTERN.matcher(encodedText); - int lastEnd = 0; - - while (matcher.find()) { - // Add any text before the encoded part - result.append(encodedText, lastEnd, matcher.start()); - - String charset = matcher.group(1); - String encoding = matcher.group(2).toUpperCase(); - String encodedValue = matcher.group(3); - - try { - String decodedValue; - if ("B".equals(encoding)) { - // Base64 decoding - byte[] decodedBytes = Base64.getDecoder().decode(encodedValue); - decodedValue = new String(decodedBytes, Charset.forName(charset)); - } else if ("Q".equals(encoding)) { - // Quoted-printable decoding - decodedValue = decodeQuotedPrintable(encodedValue, charset); - } else { - // Unknown encoding, keep original - decodedValue = matcher.group(0); - } - result.append(decodedValue); - } catch (Exception e) { - log.warn("Failed to decode MIME header part: {}", matcher.group(0), e); - // If decoding fails, keep the original encoded text - result.append(matcher.group(0)); - } - - lastEnd = matcher.end(); - } - - // Add any remaining text after the last encoded part - result.append(encodedText.substring(lastEnd)); - - return result.toString(); - } catch (Exception e) { - log.warn("Error decoding MIME header: {}", encodedText, e); - return encodedText; // Return original if decoding fails - } - } - - private static String decodeQuotedPrintable(String encodedText, String charset) { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < encodedText.length(); i++) { - char c = encodedText.charAt(i); - switch (c) { - case '=' -> { - if (i + 2 < encodedText.length()) { - String hex = encodedText.substring(i + 1, i + 3); - try { - int value = Integer.parseInt(hex, 16); - result.append((char) value); - i += 2; // Skip the hex digits - } catch (NumberFormatException e) { - // If hex parsing fails, keep the original character - result.append(c); - } - } else { - result.append(c); - } - } - case '_' -> // In RFC 2047, underscore represents space - result.append(' '); - default -> result.append(c); - } - } - - // Convert bytes to proper charset - byte[] bytes = result.toString().getBytes(StandardCharsets.ISO_8859_1); - return new String(bytes, Charset.forName(charset)); - } - - private static String safeMimeDecode(String headerValue) { - if (headerValue == null) { - return ""; - } - - try { - if (isJakartaMailAvailable()) { - // Use Jakarta Mail's MimeUtility for proper MIME decoding - Class mimeUtilityClass = Class.forName("jakarta.mail.internet.MimeUtility"); - Method decodeText = mimeUtilityClass.getMethod("decodeText", String.class); - return (String) decodeText.invoke(null, headerValue.trim()); - } else { - // Fallback to basic MIME decoding - return decodeMimeHeader(headerValue.trim()); - } - } catch (Exception e) { - log.warn("Failed to decode MIME header, using original: {}", headerValue, e); - return headerValue; - } - } - - private static boolean isValidJakartaMailPart(Object part) { - if (part == null) return false; - - try { - // Check if the object implements jakarta.mail.Part interface - Class partInterface = Class.forName("jakarta.mail.Part"); - if (!partInterface.isInstance(part)) { - return false; - } - - // Additional check for MimePart - try { - Class mimePartInterface = Class.forName("jakarta.mail.internet.MimePart"); - return mimePartInterface.isInstance(part); - } catch (ClassNotFoundException e) { - // MimePart not available, but Part is sufficient - return true; - } - } catch (ClassNotFoundException e) { - log.debug("Jakarta Mail Part interface not available for validation"); - return false; - } - } - - private static boolean isValidJakartaMailMultipart(Object multipart) { - if (multipart == null) return false; - - try { - // Check if the object implements jakarta.mail.Multipart interface - Class multipartInterface = Class.forName("jakarta.mail.Multipart"); - if (!multipartInterface.isInstance(multipart)) { - return false; - } - - // Additional check for MimeMultipart - try { - Class mimeMultipartClass = Class.forName("jakarta.mail.internet.MimeMultipart"); - if (mimeMultipartClass.isInstance(multipart)) { - log.debug("Found MimeMultipart instance for enhanced processing"); - return true; - } - } catch (ClassNotFoundException e) { - log.debug("MimeMultipart not available, using base Multipart interface"); - } - - return true; - } catch (ClassNotFoundException e) { - log.debug("Jakarta Mail Multipart interface not available for validation"); - return false; - } - } - - @Data - public static class EmailContent { - private String subject; - private String from; - private String to; - private Date date; - private String htmlBody; - private String textBody; - private int attachmentCount; - private List attachments = new ArrayList<>(); - - public void setHtmlBody(String htmlBody) { - this.htmlBody = htmlBody != null ? htmlBody.replaceAll("\r", "") : null; - } - - public void setTextBody(String textBody) { - this.textBody = textBody != null ? textBody.replaceAll("\r", "") : null; - } - } - - @Data - public static class EmailAttachment { - private String filename; - private String contentType; - private byte[] data; - private boolean embedded; - private String embeddedFilename; - private long sizeBytes; - - // New fields for advanced processing - private String contentId; - private String disposition; - private String transferEncoding; - - // Custom setter to maintain size calculation logic - public void setData(byte[] data) { - this.data = data; - if (data != null) { - this.sizeBytes = data.length; - } - } - } - - @Data - public static class MarkerPosition { - private int pageIndex; - private float x; - private float y; - private String character; - - public MarkerPosition(int pageIndex, float x, float y, String character) { - this.pageIndex = pageIndex; - this.x = x; - this.y = y; - this.character = character; - } - } - - public static class AttachmentMarkerPositionFinder - extends org.apache.pdfbox.text.PDFTextStripper { - @Getter private final List positions = new ArrayList<>(); - private int currentPageIndex; - protected boolean sortByPosition; - private boolean isInAttachmentSection; - private boolean attachmentSectionFound; - - public AttachmentMarkerPositionFinder() { - super(); - this.currentPageIndex = 0; - this.sortByPosition = false; - this.isInAttachmentSection = false; - this.attachmentSectionFound = false; - } - - @Override - protected void startPage(org.apache.pdfbox.pdmodel.PDPage page) throws IOException { - super.startPage(page); - } - - @Override - protected void endPage(org.apache.pdfbox.pdmodel.PDPage page) throws IOException { - currentPageIndex++; - super.endPage(page); - } - - @Override - protected void writeString( - String string, List textPositions) - throws IOException { - // Check if we are entering or exiting the attachment section - String lowerString = string.toLowerCase(); - - // Look for attachment section start marker - if (lowerString.contains("attachments (")) { - isInAttachmentSection = true; - attachmentSectionFound = true; - } - - // Look for attachment section end markers (common patterns that indicate end of - // attachments) - if (isInAttachmentSection - && (lowerString.contains("") - || lowerString.contains("") - || (attachmentSectionFound - && lowerString.trim().isEmpty() - && string.length() > 50))) { - isInAttachmentSection = false; - } - - // Only look for markers if we are in the attachment section - if (isInAttachmentSection) { - String attachmentMarker = MimeConstants.ATTACHMENT_MARKER; - for (int i = 0; (i = string.indexOf(attachmentMarker, i)) != -1; i++) { - if (i < textPositions.size()) { - org.apache.pdfbox.text.TextPosition textPosition = textPositions.get(i); - MarkerPosition position = - new MarkerPosition( - currentPageIndex, - textPosition.getXDirAdj(), - textPosition.getYDirAdj(), - attachmentMarker); - positions.add(position); - } - } - } - super.writeString(string, textPositions); - } - - @Override - public void setSortByPosition(boolean sortByPosition) { - this.sortByPosition = sortByPosition; - } - } } diff --git a/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java new file mode 100644 index 000000000..2478aad94 --- /dev/null +++ b/app/common/src/main/java/stirling/software/common/util/PdfAttachmentHandler.java @@ -0,0 +1,680 @@ +package stirling.software.common.util; + +import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentCatalog; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PageMode; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification; +import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationFileAttachment; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.text.PDFTextStripper; +import org.apache.pdfbox.text.TextPosition; +import org.jetbrains.annotations.NotNull; +import org.springframework.web.multipart.MultipartFile; + +import lombok.Data; +import lombok.Getter; +import lombok.experimental.UtilityClass; + +import stirling.software.common.service.CustomPDFDocumentFactory; + +@UtilityClass +public class PdfAttachmentHandler { + // Note: This class is designed for EML attachments, not general PDF attachments. + + private static final String ATTACHMENT_MARKER = "@"; + private static final float ATTACHMENT_ICON_WIDTH = 12f; + private static final float ATTACHMENT_ICON_HEIGHT = 14f; + private static final float ANNOTATION_X_OFFSET = 2f; + private static final float ANNOTATION_Y_OFFSET = 10f; + + public static byte[] attachFilesToPdf( + byte[] pdfBytes, + List attachments, + CustomPDFDocumentFactory pdfDocumentFactory) + throws IOException { + + if (attachments == null || attachments.isEmpty()) { + return pdfBytes; + } + + try (PDDocument document = pdfDocumentFactory.load(pdfBytes); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + + List multipartAttachments = new ArrayList<>(attachments.size()); + for (int i = 0; i < attachments.size(); i++) { + EmlParser.EmailAttachment attachment = attachments.get(i); + if (attachment.getData() != null && attachment.getData().length > 0) { + String embeddedFilename = + attachment.getFilename() != null + ? attachment.getFilename() + : ("attachment_" + i); + attachment.setEmbeddedFilename(embeddedFilename); + multipartAttachments.add(createMultipartFile(attachment)); + } + } + + if (!multipartAttachments.isEmpty()) { + Map indexToFilenameMap = + addAttachmentsToDocumentWithMapping( + document, multipartAttachments, attachments); + setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS); + addAttachmentAnnotationsToDocumentWithMapping( + document, attachments, indexToFilenameMap); + } + + document.save(outputStream); + return outputStream.toByteArray(); + } catch (RuntimeException e) { + throw new IOException( + "Invalid PDF structure or processing error: " + e.getMessage(), e); + } catch (Exception e) { + throw new IOException("Error attaching files to PDF: " + e.getMessage(), e); + } + } + + private static MultipartFile createMultipartFile(EmlParser.EmailAttachment attachment) { + return new MultipartFile() { + @Override + public @NotNull String getName() { + return "attachment"; + } + + @Override + public String getOriginalFilename() { + return attachment.getFilename() != null + ? attachment.getFilename() + : "attachment_" + System.currentTimeMillis(); + } + + @Override + public String getContentType() { + return attachment.getContentType() != null + ? attachment.getContentType() + : "application/octet-stream"; + } + + @Override + public boolean isEmpty() { + return attachment.getData() == null || attachment.getData().length == 0; + } + + @Override + public long getSize() { + return attachment.getData() != null ? attachment.getData().length : 0; + } + + @Override + public byte @NotNull [] getBytes() { + return attachment.getData() != null ? attachment.getData() : new byte[0]; + } + + @Override + public @NotNull InputStream getInputStream() { + byte[] data = attachment.getData(); + return new ByteArrayInputStream(data != null ? data : new byte[0]); + } + + @Override + public void transferTo(@NotNull File dest) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(dest)) { + byte[] data = attachment.getData(); + if (data != null) { + fos.write(data); + } + } + } + }; + } + + private static String ensureUniqueFilename(String filename, Set existingNames) { + if (!existingNames.contains(filename)) { + return filename; + } + + String baseName; + String extension = ""; + int lastDot = filename.lastIndexOf('.'); + if (lastDot > 0) { + baseName = filename.substring(0, lastDot); + extension = filename.substring(lastDot); + } else { + baseName = filename; + } + + int counter = 1; + String uniqueName; + do { + uniqueName = baseName + "_" + counter + extension; + counter++; + } while (existingNames.contains(uniqueName)); + + return uniqueName; + } + + private static @NotNull PDRectangle calculateAnnotationRectangle( + PDPage page, float x, float y) { + PDRectangle cropBox = page.getCropBox(); + + // ISO 32000-1:2008 Section 8.3: PDF coordinate system transforms + int rotation = page.getRotation(); + float pdfX = x; + float pdfY = cropBox.getHeight() - y; + + switch (rotation) { + case 90 -> { + float temp = pdfX; + pdfX = pdfY; + pdfY = cropBox.getWidth() - temp; + } + case 180 -> { + pdfX = cropBox.getWidth() - pdfX; + pdfY = y; + } + case 270 -> { + float temp = pdfX; + pdfX = cropBox.getHeight() - pdfY; + pdfY = temp; + } + default -> {} + } + + float iconHeight = ATTACHMENT_ICON_HEIGHT; + float paddingX = 2.0f; + float paddingY = 2.0f; + + PDRectangle rect = + new PDRectangle( + pdfX + ANNOTATION_X_OFFSET + paddingX, + pdfY - iconHeight + ANNOTATION_Y_OFFSET + paddingY, + ATTACHMENT_ICON_WIDTH, + iconHeight); + + PDRectangle mediaBox = page.getMediaBox(); + if (rect.getLowerLeftX() < mediaBox.getLowerLeftX() + || rect.getLowerLeftY() < mediaBox.getLowerLeftY() + || rect.getUpperRightX() > mediaBox.getUpperRightX() + || rect.getUpperRightY() > mediaBox.getUpperRightY()) { + + float adjustedX = + Math.max( + mediaBox.getLowerLeftX(), + Math.min( + rect.getLowerLeftX(), + mediaBox.getUpperRightX() - rect.getWidth())); + float adjustedY = + Math.max( + mediaBox.getLowerLeftY(), + Math.min( + rect.getLowerLeftY(), + mediaBox.getUpperRightY() - rect.getHeight())); + rect = new PDRectangle(adjustedX, adjustedY, rect.getWidth(), rect.getHeight()); + } + + return rect; + } + + public static String processInlineImages( + String htmlContent, EmlParser.EmailContent emailContent) { + if (htmlContent == null || emailContent == null) return htmlContent; + + Map contentIdMap = new HashMap<>(); + for (EmlParser.EmailAttachment attachment : emailContent.getAttachments()) { + if (attachment.isEmbedded() + && attachment.getContentId() != null + && attachment.getData() != null) { + contentIdMap.put(attachment.getContentId(), attachment); + } + } + + if (contentIdMap.isEmpty()) return htmlContent; + + Pattern cidPattern = + Pattern.compile( + "(?i)]*\\ssrc\\s*=\\s*['\"]cid:([^'\"]+)['\"][^>]*>", + Pattern.CASE_INSENSITIVE); + Matcher matcher = cidPattern.matcher(htmlContent); + + StringBuilder result = new StringBuilder(); + while (matcher.find()) { + String contentId = matcher.group(1); + EmlParser.EmailAttachment attachment = contentIdMap.get(contentId); + + if (attachment != null && attachment.getData() != null) { + String mimeType = + EmlProcessingUtils.detectMimeType( + attachment.getFilename(), attachment.getContentType()); + + String base64Data = Base64.getEncoder().encodeToString(attachment.getData()); + String dataUri = "data:" + mimeType + ";base64," + base64Data; + + String replacement = + matcher.group(0).replaceFirst("cid:" + Pattern.quote(contentId), dataUri); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } else { + matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(0))); + } + } + matcher.appendTail(result); + + return result.toString(); + } + + public static String formatEmailDate(Date date) { + if (date == null) return ""; + + SimpleDateFormat formatter = + new SimpleDateFormat("EEE, MMM d, yyyy 'at' h:mm a z", Locale.ENGLISH); + formatter.setTimeZone(TimeZone.getTimeZone("UTC")); + return formatter.format(date); + } + + @Data + public static class MarkerPosition { + private int pageIndex; + private float x; + private float y; + private String character; + private String filename; + + public MarkerPosition(int pageIndex, float x, float y, String character, String filename) { + this.pageIndex = pageIndex; + this.x = x; + this.y = y; + this.character = character; + this.filename = filename; + } + } + + public static class AttachmentMarkerPositionFinder extends PDFTextStripper { + @Getter private final List positions = new ArrayList<>(); + private int currentPageIndex; + protected boolean sortByPosition; + private boolean isInAttachmentSection; + private boolean attachmentSectionFound; + private final StringBuilder currentText = new StringBuilder(); + + private static final Pattern ATTACHMENT_SECTION_PATTERN = + Pattern.compile("attachments\\s*\\(\\d+\\)", Pattern.CASE_INSENSITIVE); + + private static final Pattern FILENAME_PATTERN = + Pattern.compile("@\\s*([^\\s\\(]+(?:\\.[a-zA-Z0-9]+)?)"); + + public AttachmentMarkerPositionFinder() { + super(); + this.currentPageIndex = 0; + this.sortByPosition = false; // Disable sorting to preserve document order + this.isInAttachmentSection = false; + this.attachmentSectionFound = false; + } + + @Override + public String getText(PDDocument document) throws IOException { + super.getText(document); + + if (sortByPosition) { + positions.sort( + (a, b) -> { + int pageCompare = Integer.compare(a.getPageIndex(), b.getPageIndex()); + if (pageCompare != 0) return pageCompare; + return Float.compare( + b.getY(), a.getY()); // Descending Y per PDF coordinate system + }); + } + + return ""; // Return empty string as we only need positions + } + + @Override + protected void startPage(PDPage page) throws IOException { + super.startPage(page); + } + + @Override + protected void endPage(PDPage page) throws IOException { + currentPageIndex++; + super.endPage(page); + } + + @Override + protected void writeString(String string, List textPositions) + throws IOException { + String lowerString = string.toLowerCase(); + + if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) { + isInAttachmentSection = true; + attachmentSectionFound = true; + } + + if (isInAttachmentSection + && (lowerString.contains("") + || lowerString.contains("") + || (attachmentSectionFound + && lowerString.trim().isEmpty() + && string.length() > 50))) { + isInAttachmentSection = false; + } + + if (isInAttachmentSection) { + currentText.append(string); + + for (int i = 0; (i = string.indexOf(ATTACHMENT_MARKER, i)) != -1; i++) { + if (i < textPositions.size()) { + TextPosition textPosition = textPositions.get(i); + + String filename = extractFilenameAfterMarker(string, i); + + MarkerPosition position = + new MarkerPosition( + currentPageIndex, + textPosition.getXDirAdj(), + textPosition.getYDirAdj(), + ATTACHMENT_MARKER, + filename); + positions.add(position); + } + } + } + super.writeString(string, textPositions); + } + + @Override + public void setSortByPosition(boolean sortByPosition) { + this.sortByPosition = sortByPosition; + } + + private String extractFilenameAfterMarker(String text, int markerIndex) { + String afterMarker = text.substring(markerIndex + 1); + + Matcher matcher = FILENAME_PATTERN.matcher("@" + afterMarker); + if (matcher.find()) { + return matcher.group(1); + } + + String[] parts = afterMarker.split("[\\s\\(\\)]+"); + for (String part : parts) { + part = part.trim(); + if (part.length() > 3 && part.contains(".")) { + return part; + } + } + + return null; + } + } + + private static Map addAttachmentsToDocumentWithMapping( + PDDocument document, + List attachments, + List originalAttachments) + throws IOException { + + PDDocumentCatalog catalog = document.getDocumentCatalog(); + + if (catalog == null) { + throw new IOException("PDF document catalog is not accessible"); + } + + PDDocumentNameDictionary documentNames = catalog.getNames(); + if (documentNames == null) { + documentNames = new PDDocumentNameDictionary(catalog); + catalog.setNames(documentNames); + } + + PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles(); + if (embeddedFilesTree == null) { + embeddedFilesTree = new PDEmbeddedFilesNameTreeNode(); + documentNames.setEmbeddedFiles(embeddedFilesTree); + } + + Map existingNames = embeddedFilesTree.getNames(); + if (existingNames == null) { + existingNames = new HashMap<>(); + } + + Map indexToFilenameMap = new HashMap<>(); + + for (int i = 0; i < attachments.size(); i++) { + MultipartFile attachment = attachments.get(i); + String filename = attachment.getOriginalFilename(); + if (filename == null || filename.trim().isEmpty()) { + filename = "attachment_" + i; + } + + String normalizedFilename = + isAscii(filename) + ? filename + : java.text.Normalizer.normalize( + filename, java.text.Normalizer.Form.NFC); + String uniqueFilename = + ensureUniqueFilename(normalizedFilename, existingNames.keySet()); + + indexToFilenameMap.put(i, uniqueFilename); + + PDEmbeddedFile embeddedFile = new PDEmbeddedFile(document, attachment.getInputStream()); + embeddedFile.setSize((int) attachment.getSize()); + + GregorianCalendar currentTime = new GregorianCalendar(); + embeddedFile.setCreationDate(currentTime); + embeddedFile.setModDate(currentTime); + + String contentType = attachment.getContentType(); + if (contentType != null && !contentType.trim().isEmpty()) { + embeddedFile.setSubtype(contentType); + } + + PDComplexFileSpecification fileSpecification = new PDComplexFileSpecification(); + fileSpecification.setFile(uniqueFilename); + fileSpecification.setFileUnicode(uniqueFilename); + fileSpecification.setEmbeddedFile(embeddedFile); + fileSpecification.setEmbeddedFileUnicode(embeddedFile); + + existingNames.put(uniqueFilename, fileSpecification); + } + + embeddedFilesTree.setNames(existingNames); + documentNames.setEmbeddedFiles(embeddedFilesTree); + catalog.setNames(documentNames); + + return indexToFilenameMap; + } + + private static void addAttachmentAnnotationsToDocumentWithMapping( + PDDocument document, + List attachments, + Map indexToFilenameMap) + throws IOException { + + if (document.getNumberOfPages() == 0 || attachments == null || attachments.isEmpty()) { + return; + } + + AttachmentMarkerPositionFinder finder = new AttachmentMarkerPositionFinder(); + finder.setSortByPosition(false); // Keep document order to maintain pairing + finder.getText(document); + List markerPositions = finder.getPositions(); + + int annotationsToAdd = Math.min(markerPositions.size(), attachments.size()); + + for (int i = 0; i < annotationsToAdd; i++) { + MarkerPosition position = markerPositions.get(i); + + String filenameNearMarker = position.getFilename(); + + EmlParser.EmailAttachment matchingAttachment = + findAttachmentByFilename(attachments, filenameNearMarker); + + if (matchingAttachment != null) { + String embeddedFilename = + findEmbeddedFilenameForAttachment(matchingAttachment, indexToFilenameMap); + + if (embeddedFilename != null) { + PDPage page = document.getPage(position.getPageIndex()); + addAttachmentAnnotationToPageWithMapping( + document, + page, + matchingAttachment, + embeddedFilename, + position.getX(), + position.getY(), + i); + } else { + // No embedded filename found for attachment + } + } else { + // No matching attachment found for filename near marker + } + } + } + + private static EmlParser.EmailAttachment findAttachmentByFilename( + List attachments, String targetFilename) { + if (targetFilename == null || targetFilename.trim().isEmpty()) { + return null; + } + + String normalizedTarget = normalizeFilename(targetFilename); + + // First try exact match + for (EmlParser.EmailAttachment attachment : attachments) { + if (attachment.getFilename() != null) { + String normalizedAttachment = normalizeFilename(attachment.getFilename()); + if (normalizedAttachment.equals(normalizedTarget)) { + return attachment; + } + } + } + + // Then try contains match + for (EmlParser.EmailAttachment attachment : attachments) { + if (attachment.getFilename() != null) { + String normalizedAttachment = normalizeFilename(attachment.getFilename()); + if (normalizedAttachment.contains(normalizedTarget) + || normalizedTarget.contains(normalizedAttachment)) { + return attachment; + } + } + } + + return null; + } + + private static String findEmbeddedFilenameForAttachment( + EmlParser.EmailAttachment attachment, Map indexToFilenameMap) { + + String attachmentFilename = attachment.getFilename(); + if (attachmentFilename == null) { + return null; + } + + for (Map.Entry entry : indexToFilenameMap.entrySet()) { + String embeddedFilename = entry.getValue(); + if (embeddedFilename != null + && (embeddedFilename.equals(attachmentFilename) + || embeddedFilename.contains(attachmentFilename) + || attachmentFilename.contains(embeddedFilename))) { + return embeddedFilename; + } + } + + return null; + } + + private static String normalizeFilename(String filename) { + if (filename == null) return ""; + return filename.toLowerCase() + .trim() + .replaceAll("\\s+", " ") + .replaceAll("[^a-zA-Z0-9._-]", ""); + } + + private static void addAttachmentAnnotationToPageWithMapping( + PDDocument document, + PDPage page, + EmlParser.EmailAttachment attachment, + String embeddedFilename, + float x, + float y, + int attachmentIndex) + throws IOException { + + PDAnnotationFileAttachment fileAnnotation = new PDAnnotationFileAttachment(); + + PDRectangle rect = calculateAnnotationRectangle(page, x, y); + fileAnnotation.setRectangle(rect); + + fileAnnotation.setPrinted(false); + fileAnnotation.setHidden(false); + fileAnnotation.setNoView(false); + fileAnnotation.setNoZoom(true); + fileAnnotation.setNoRotate(true); + + try { + PDAppearanceDictionary appearance = new PDAppearanceDictionary(); + PDAppearanceStream normalAppearance = new PDAppearanceStream(document); + normalAppearance.setBBox(new PDRectangle(0, 0, rect.getWidth(), rect.getHeight())); + appearance.setNormalAppearance(normalAppearance); + fileAnnotation.setAppearance(appearance); + } catch (RuntimeException e) { + fileAnnotation.setAppearance(null); + } + + PDEmbeddedFilesNameTreeNode efTree = + document.getDocumentCatalog().getNames().getEmbeddedFiles(); + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + PDComplexFileSpecification fileSpec = efMap.get(embeddedFilename); + if (fileSpec != null) { + fileAnnotation.setFile(fileSpec); + } else { + // Could not find embedded file + } + } + } + + fileAnnotation.setContents( + "Attachment " + (attachmentIndex + 1) + ": " + attachment.getFilename()); + fileAnnotation.setAnnotationName( + "EmbeddedFile_" + attachmentIndex + "_" + embeddedFilename); + + page.getAnnotations().add(fileAnnotation); + } + + private static boolean isAscii(String str) { + if (str == null) return true; + for (int i = 0; i < str.length(); i++) { + if (str.charAt(i) > 127) { + return false; + } + } + return true; + } +} From 774b500159e643cd06d8090eacd78832dc524388 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 14:19:19 +0100 Subject: [PATCH 11/23] get updates advanced (#4124) # Description of Changes This pull request introduces a comprehensive update to the application's update notification and modal system, enhancing both the backend logic and the user interface for update alerts. The changes include a new modal dialog for update details, improved internationalization (i18n) support, dynamic fetching of update information, and context-aware download links. These improvements make update notifications clearer, more informative, and tailored to the user's installation type. **Key changes:** **1. Update Notification and Modal System Overhaul** - Added a new modal dialog (`showUpdateModal`) that displays detailed update information, including current, latest, and latest stable versions, update priority, breaking changes, migration guides, and a list of available updates. The modal dynamically fetches and displays full update details and adapts to dark mode. ([[app/core/src/main/resources/static/js/githubVersion.jsR206-R387](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) - Enhanced the update button logic to reflect update priority visually (e.g., urgent/normal/minor), store summary data, and trigger the modal on click. ([[app/core/src/main/resources/static/js/githubVersion.jsL74-R190](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) - Improved the update check process to use a new summary API endpoint and handle missing or failed update data gracefully. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL74-R190)]) **2. Context-Aware Download Links** - Introduced `getDownloadUrl()` to generate download links based on the user's machine type and security configuration, ensuring only relevant installers or jars are offered. ([[app/core/src/main/resources/static/js/githubVersion.jsL19-R108](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aL19-R108)]) **3. Internationalization (i18n) Enhancements** - Added new i18n keys for all update-related modal and notification strings in `messages_en_GB.properties`. ([[app/core/src/main/resources/messages_en_GB.propertiesR369-R400](diffhunk://#diff-ee1c6999a33498cfa3abba4a384e73a8b8269856899438de80560c965079a9fdR369-R400)]) - Injected all necessary i18n constants into the frontend via `navbar.html` for use in the modal and notifications. ([[app/core/src/main/resources/templates/fragments/navbar.htmlR14-R51](diffhunk://#diff-e7ef383033ea52a00c96e71d5d2c1ff08829078fa5c84c8e48e1bf8f48861ec6R14-R51)]) **4. General UI and Code Improvements** - Ensured update button styling is reset before applying new styles and improved accessibility by hiding the settings modal when the update modal is shown. [[1]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR138)], [[2]](diffhunk://#diff-5a6376050581cc6f1fb0b6266af4d8a3db1332879459afd3a073b274b5ab637aR206-R387)]) These changes collectively provide a more robust, user-friendly, and maintainable update notification experience. --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Reece Browne Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Co-authored-by: a --- .../main/resources/messages_en_GB.properties | 32 ++ .../main/resources/static/js/githubVersion.js | 314 +++++++++++++++++- .../resources/templates/fragments/navbar.html | 35 ++ build.gradle | 2 +- 4 files changed, 368 insertions(+), 15 deletions(-) diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 37be2c06a..f619b7b6e 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Settings settings.update=Update available settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Choose download option (For single file non zip downloads): settings.downloadOption.1=Open in same window diff --git a/app/core/src/main/resources/static/js/githubVersion.js b/app/core/src/main/resources/static/js/githubVersion.js index 2aef90d8c..ffc22ed08 100644 --- a/app/core/src/main/resources/static/js/githubVersion.js +++ b/app/core/src/main/resources/static/js/githubVersion.js @@ -16,21 +16,96 @@ function compareVersions(version1, version2) { return 0; } -async function getLatestReleaseVersion() { - const url = "https://api.github.com/repos/Stirling-Tools/Stirling-PDF/releases/latest"; +function getDownloadUrl() { + // Only show download for non-Docker installations + if (machineType === 'Docker' || machineType === 'Kubernetes') { + return null; + } + + const baseUrl = 'https://files.stirlingpdf.com/'; + + // Determine file based on machine type and security + if (machineType === 'Server-jar') { + return baseUrl + (activeSecurity ? 'Stirling-PDF-with-login.jar' : 'Stirling-PDF.jar'); + } + + // Client installations + if (machineType.startsWith('Client-')) { + const os = machineType.replace('Client-', ''); // win, mac, unix + const type = activeSecurity ? '-server-security' : '-server'; + + if (os === 'unix') { + return baseUrl + os + type + '.jar'; + } else if (os === 'win') { + return baseUrl + os + '-installer.exe'; + } else if (os === 'mac') { + return baseUrl + os + '-installer.dmg'; + } + } + + return null; +} + +// Function to get translated priority text +function getTranslatedPriority(priority) { + switch(priority?.toLowerCase()) { + case 'urgent': return updatePriorityUrgent; + case 'normal': return updatePriorityNormal; + case 'minor': return updatePriorityMinor; + case 'low': return updatePriorityLow; + default: return priority?.toUpperCase() || 'NORMAL'; + } +} + +async function getUpdateSummary() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=true`; + console.log("Fetching update summary from:", url); try { const response = await fetch(url); + console.log("Response status:", response.status); if (response.status === 200) { const data = await response.json(); - return data.tag_name ? data.tag_name.substring(1) : ""; + return data; } else { - // If the status is not 200, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", response.status); + return null; } } catch (error) { - console.error("Failed to fetch latest version from GitHub:", error); - // If an error occurs, try to get the version from build.gradle - return await getCurrentVersionFromBypass(); + console.error("Failed to fetch update summary from Supabase:", error); + return null; + } +} + +async function getFullUpdateInfo() { + // Map Java License enum to API types + let type = 'normal'; + if (licenseType === 'PRO') { + type = 'pro'; + } else if (licenseType === 'ENTERPRISE') { + type = 'enterprise'; + } + const url = `https://supabase.stirling.com/functions/v1/updates?from=${currentVersion}&type=${type}&login=${activeSecurity}&summary=false`; + console.log("Fetching full update info from:", url); + try { + const response = await fetch(url); + console.log("Full update response status:", response.status); + if (response.status === 200) { + const data = await response.json(); + return data; + } else { + console.error("Failed to fetch full update info from Supabase:", response.status); + return null; + } + } catch (error) { + console.error("Failed to fetch full update info from Supabase:", error); + return null; } } @@ -60,6 +135,7 @@ async function checkForUpdate() { var updateLinkLegacy = document.getElementById("update-link-legacy") || null; if (updateBtn !== null) { updateBtn.style.display = "none"; + updateBtn.classList.remove("btn-danger", "btn-warning", "btn-outline-primary"); } if (updateLink !== null) { updateLink.style.display = "none"; @@ -71,19 +147,47 @@ async function checkForUpdate() { } } - const latestVersion = await getLatestReleaseVersion(); - console.log("latestVersion=" + latestVersion); + const updateSummary = await getUpdateSummary(); + if (!updateSummary) { + console.log("No update summary available"); + return; + } + + console.log("updateSummary=", updateSummary); console.log("currentVersion=" + currentVersion); - console.log("compareVersions(latestVersion, currentVersion) > 0)=" + compareVersions(latestVersion, currentVersion)); - if (latestVersion && compareVersions(latestVersion, currentVersion) > 0) { + console.log("latestVersion=" + updateSummary.latest_version); + + if (updateSummary.latest_version && compareVersions(updateSummary.latest_version, currentVersion) > 0) { + const priority = updateSummary.max_priority || 'normal'; + if (updateBtn != null) { - document.getElementById("update-btn").style.display = "block"; + // Style button based on priority + if (priority === 'urgent') { + updateBtn.classList.add("btn-danger"); + updateBtn.innerHTML = urgentUpdateAvailable; + } else if (priority === 'normal') { + updateBtn.classList.add("btn-warning"); + updateBtn.innerHTML = updateAvailableText; + } else { + updateBtn.classList.add("btn-outline-primary"); + updateBtn.innerHTML = updateAvailableText; + } + + // Store summary for initial display + updateBtn.setAttribute('data-update-summary', JSON.stringify(updateSummary)); + updateBtn.style.display = "block"; + + // Add click handler for update details modal + updateBtn.onclick = function(e) { + e.preventDefault(); + showUpdateModal(); + }; } if (updateLink !== null) { document.getElementById("update-link").style.display = "flex"; } if (updateLinkLegacy !== null) { - document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + latestVersion + ''); + document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '' + currentVersion + '').replace("{1}", '' + updateSummary.latest_version + ''); if (updateLinkLegacy.classList.contains("visually-hidden")) { updateLinkLegacy.classList.remove("visually-hidden"); } @@ -99,6 +203,188 @@ async function checkForUpdate() { } } +async function showUpdateModal() { + // Close settings modal if open + const settingsModal = bootstrap.Modal.getInstance(document.getElementById('settingsModal')); + if (settingsModal) { + settingsModal.hide(); + } + + // Get summary data from button + const updateBtn = document.getElementById("update-btn"); + const summaryData = JSON.parse(updateBtn.getAttribute('data-update-summary')); + + // Utility function to escape HTML special characters + function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + // Create initial modal with loading state + const initialModalHtml = ` + + `; + + // Remove existing modal if present + const existingModal = document.getElementById('updateModal'); + if (existingModal) { + existingModal.remove(); + } + + // Add modal to body + document.body.insertAdjacentHTML('beforeend', initialModalHtml); + + // Show modal + const modal = new bootstrap.Modal(document.getElementById('updateModal')); + modal.show(); + + // Fetch full update info + const fullUpdateInfo = await getFullUpdateInfo(); + + // Update modal with full information + const modalBody = document.getElementById('updateModalBody'); + if (fullUpdateInfo && fullUpdateInfo.new_versions) { + const storedMode = localStorage.getItem("dark-mode"); + const isDarkMode = storedMode === "on" || + (storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); + const darkClasses = isDarkMode ? { + accordionItem: 'bg-dark border-secondary text-light', + accordionButton: 'bg-dark text-light border-secondary', + accordionBody: 'bg-dark text-light' + } : { + accordionItem: '', + accordionButton: '', + accordionBody: '' + }; + + const detailedVersionsHtml = ` +
+
${updateAvailableUpdates}
+
+ ${fullUpdateInfo.new_versions.map((version, index) => ` +
+

+ +

+
+
+
${version.announcement.title}
+

${version.announcement.message}

+ ${version.compatibility.breaking_changes ? ` + + ` : ''} +
+
+
+ `).join('')} +
+
+ `; + + // Remove loading spinner and add detailed info + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.remove(); + } + modalBody.insertAdjacentHTML('beforeend', detailedVersionsHtml); + + } else { + // Remove loading spinner if failed to load + const spinner = document.getElementById('loadingSpinner'); + if (spinner) { + spinner.parentElement.innerHTML = `

${updateUnableToLoadDetails}

`; + } + } +} + document.addEventListener("DOMContentLoaded", (event) => { checkForUpdate(); }); diff --git a/app/core/src/main/resources/templates/fragments/navbar.html b/app/core/src/main/resources/templates/fragments/navbar.html index e5aea9345..833d4fd91 100644 --- a/app/core/src/main/resources/templates/fragments/navbar.html +++ b/app/core/src/main/resources/templates/fragments/navbar.html @@ -11,9 +11,44 @@ diff --git a/build.gradle b/build.gradle index ec786e2ed..39672cf24 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.1.1' + version = '1.1.2' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' From e6a77e83da2aa107b943097e50fe475615c6c665 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:36:58 +0100 Subject: [PATCH 12/23] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#4144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .github/labels.yml | 2 +- app/core/src/main/resources/static/js/githubVersion.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index b6cd969f6..a79fb8be5 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -182,4 +182,4 @@ description: "Pull request has been deployed to a test environment" - name: "codex" color: "ededed" - description: "chatgpt AI generated code" \ No newline at end of file + description: "chatgpt AI generated code" diff --git a/app/core/src/main/resources/static/js/githubVersion.js b/app/core/src/main/resources/static/js/githubVersion.js index ffc22ed08..5dee33238 100644 --- a/app/core/src/main/resources/static/js/githubVersion.js +++ b/app/core/src/main/resources/static/js/githubVersion.js @@ -321,7 +321,7 @@ async function showUpdateModal() { const modalBody = document.getElementById('updateModalBody'); if (fullUpdateInfo && fullUpdateInfo.new_versions) { const storedMode = localStorage.getItem("dark-mode"); - const isDarkMode = storedMode === "on" || + const isDarkMode = storedMode === "on" || (storedMode === null && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); const darkClasses = isDarkMode ? { accordionItem: 'bg-dark border-secondary text-light', From 6675a8af990dd541bab996091fc7bd716f719387 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:37:18 +0100 Subject: [PATCH 13/23] :globe_with_meridians: Sync Translations + Update README Progress Table (#4143) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request --------- Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 78 +++++++++---------- .../main/resources/messages_ar_AR.properties | 32 ++++++++ .../main/resources/messages_az_AZ.properties | 32 ++++++++ .../main/resources/messages_bg_BG.properties | 32 ++++++++ .../main/resources/messages_bo_CN.properties | 32 ++++++++ .../main/resources/messages_ca_CA.properties | 32 ++++++++ .../main/resources/messages_cs_CZ.properties | 32 ++++++++ .../main/resources/messages_da_DK.properties | 32 ++++++++ .../main/resources/messages_de_DE.properties | 32 ++++++++ .../main/resources/messages_el_GR.properties | 32 ++++++++ .../main/resources/messages_en_US.properties | 32 ++++++++ .../main/resources/messages_es_ES.properties | 32 ++++++++ .../main/resources/messages_eu_ES.properties | 32 ++++++++ .../main/resources/messages_fa_IR.properties | 32 ++++++++ .../main/resources/messages_fr_FR.properties | 32 ++++++++ .../main/resources/messages_ga_IE.properties | 32 ++++++++ .../main/resources/messages_hi_IN.properties | 32 ++++++++ .../main/resources/messages_hr_HR.properties | 32 ++++++++ .../main/resources/messages_hu_HU.properties | 32 ++++++++ .../main/resources/messages_id_ID.properties | 32 ++++++++ .../main/resources/messages_it_IT.properties | 32 ++++++++ .../main/resources/messages_ja_JP.properties | 32 ++++++++ .../main/resources/messages_ko_KR.properties | 32 ++++++++ .../main/resources/messages_ml_IN.properties | 32 ++++++++ .../main/resources/messages_nl_NL.properties | 32 ++++++++ .../main/resources/messages_no_NB.properties | 32 ++++++++ .../main/resources/messages_pl_PL.properties | 32 ++++++++ .../main/resources/messages_pt_BR.properties | 32 ++++++++ .../main/resources/messages_pt_PT.properties | 32 ++++++++ .../main/resources/messages_ro_RO.properties | 32 ++++++++ .../main/resources/messages_ru_RU.properties | 32 ++++++++ .../main/resources/messages_sk_SK.properties | 32 ++++++++ .../main/resources/messages_sl_SI.properties | 32 ++++++++ .../resources/messages_sr_LATN_RS.properties | 32 ++++++++ .../main/resources/messages_sv_SE.properties | 32 ++++++++ .../main/resources/messages_th_TH.properties | 32 ++++++++ .../main/resources/messages_tr_TR.properties | 32 ++++++++ .../main/resources/messages_uk_UA.properties | 32 ++++++++ .../main/resources/messages_vi_VN.properties | 32 ++++++++ .../main/resources/messages_zh_CN.properties | 32 ++++++++ .../main/resources/messages_zh_TW.properties | 32 ++++++++ 41 files changed, 1319 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index b9660ce43..3b582cbfc 100644 --- a/README.md +++ b/README.md @@ -116,47 +116,47 @@ Stirling-PDF currently supports 40 languages! | Language | Progress | | -------------------------------------------- | -------------------------------------- | -| Arabic (العربية) (ar_AR) | ![63%](https://geps.dev/progress/63) | -| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![63%](https://geps.dev/progress/63) | -| Basque (Euskara) (eu_ES) | ![37%](https://geps.dev/progress/37) | -| Bulgarian (Български) (bg_BG) | ![70%](https://geps.dev/progress/70) | -| Catalan (Català) (ca_CA) | ![69%](https://geps.dev/progress/69) | -| Croatian (Hrvatski) (hr_HR) | ![62%](https://geps.dev/progress/62) | -| Czech (Česky) (cs_CZ) | ![71%](https://geps.dev/progress/71) | -| Danish (Dansk) (da_DK) | ![63%](https://geps.dev/progress/63) | -| Dutch (Nederlands) (nl_NL) | ![61%](https://geps.dev/progress/61) | +| Arabic (العربية) (ar_AR) | ![61%](https://geps.dev/progress/61) | +| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![62%](https://geps.dev/progress/62) | +| Basque (Euskara) (eu_ES) | ![36%](https://geps.dev/progress/36) | +| Bulgarian (Български) (bg_BG) | ![68%](https://geps.dev/progress/68) | +| Catalan (Català) (ca_CA) | ![68%](https://geps.dev/progress/68) | +| Croatian (Hrvatski) (hr_HR) | ![60%](https://geps.dev/progress/60) | +| Czech (Česky) (cs_CZ) | ![70%](https://geps.dev/progress/70) | +| Danish (Dansk) (da_DK) | ![61%](https://geps.dev/progress/61) | +| Dutch (Nederlands) (nl_NL) | ![60%](https://geps.dev/progress/60) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | -| French (Français) (fr_FR) | ![91%](https://geps.dev/progress/91) | -| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | -| Greek (Ελληνικά) (el_GR) | ![69%](https://geps.dev/progress/69) | -| Hindi (हिंदी) (hi_IN) | ![68%](https://geps.dev/progress/68) | -| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | -| Indonesian (Bahasa Indonesia) (id_ID) | ![63%](https://geps.dev/progress/63) | -| Irish (Gaeilge) (ga_IE) | ![70%](https://geps.dev/progress/70) | -| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | -| Japanese (日本語) (ja_JP) | ![95%](https://geps.dev/progress/95) | -| Korean (한국어) (ko_KR) | ![69%](https://geps.dev/progress/69) | -| Norwegian (Norsk) (no_NB) | ![67%](https://geps.dev/progress/67) | -| Persian (فارسی) (fa_IR) | ![66%](https://geps.dev/progress/66) | -| Polish (Polski) (pl_PL) | ![73%](https://geps.dev/progress/73) | -| Portuguese (Português) (pt_PT) | ![70%](https://geps.dev/progress/70) | -| Portuguese Brazilian (Português) (pt_BR) | ![77%](https://geps.dev/progress/77) | -| Romanian (Română) (ro_RO) | ![59%](https://geps.dev/progress/59) | -| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) | -| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![97%](https://geps.dev/progress/97) | -| Simplified Chinese (简体中文) (zh_CN) | ![95%](https://geps.dev/progress/95) | -| Slovakian (Slovensky) (sk_SK) | ![53%](https://geps.dev/progress/53) | -| Slovenian (Slovenščina) (sl_SI) | ![73%](https://geps.dev/progress/73) | -| Spanish (Español) (es_ES) | ![75%](https://geps.dev/progress/75) | -| Swedish (Svenska) (sv_SE) | ![67%](https://geps.dev/progress/67) | -| Thai (ไทย) (th_TH) | ![60%](https://geps.dev/progress/60) | -| Tibetan (བོད་ཡིག་) (bo_CN) | ![66%](https://geps.dev/progress/66) | -| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) | -| Turkish (Türkçe) (tr_TR) | ![82%](https://geps.dev/progress/82) | -| Ukrainian (Українська) (uk_UA) | ![72%](https://geps.dev/progress/72) | -| Vietnamese (Tiếng Việt) (vi_VN) | ![58%](https://geps.dev/progress/58) | -| Malayalam (മലയാളം) (ml_IN) | ![75%](https://geps.dev/progress/75) | +| French (Français) (fr_FR) | ![89%](https://geps.dev/progress/89) | +| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) | +| Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) | +| Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) | +| Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | +| Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | +| Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | +| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) | +| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | +| Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) | +| Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) | +| Persian (فارسی) (fa_IR) | ![64%](https://geps.dev/progress/64) | +| Polish (Polski) (pl_PL) | ![72%](https://geps.dev/progress/72) | +| Portuguese (Português) (pt_PT) | ![69%](https://geps.dev/progress/69) | +| Portuguese Brazilian (Português) (pt_BR) | ![76%](https://geps.dev/progress/76) | +| Romanian (Română) (ro_RO) | ![57%](https://geps.dev/progress/57) | +| Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) | +| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![95%](https://geps.dev/progress/95) | +| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) | +| Slovakian (Slovensky) (sk_SK) | ![51%](https://geps.dev/progress/51) | +| Slovenian (Slovenščina) (sl_SI) | ![71%](https://geps.dev/progress/71) | +| Spanish (Español) (es_ES) | ![74%](https://geps.dev/progress/74) | +| Swedish (Svenska) (sv_SE) | ![65%](https://geps.dev/progress/65) | +| Thai (ไทย) (th_TH) | ![59%](https://geps.dev/progress/59) | +| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) | +| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) | +| Turkish (Türkçe) (tr_TR) | ![80%](https://geps.dev/progress/80) | +| Ukrainian (Українська) (uk_UA) | ![71%](https://geps.dev/progress/71) | +| Vietnamese (Tiếng Việt) (vi_VN) | ![57%](https://geps.dev/progress/57) | +| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) | ## Stirling PDF Enterprise diff --git a/app/core/src/main/resources/messages_ar_AR.properties b/app/core/src/main/resources/messages_ar_AR.properties index 71bedd8e2..1cd554cd1 100644 --- a/app/core/src/main/resources/messages_ar_AR.properties +++ b/app/core/src/main/resources/messages_ar_AR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=المفضل settings.title=الإعدادات settings.update=التحديث متاح settings.updateAvailable={0} هو الإصدار المثبت حاليًا. إصدار جديد ({1}) متاح. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=إصدار التطبيق: settings.downloadOption.title=تحديد خيار التنزيل (للتنزيلات ذات الملف الواحد غير المضغوط): settings.downloadOption.1=فتح في نفس النافذة diff --git a/app/core/src/main/resources/messages_az_AZ.properties b/app/core/src/main/resources/messages_az_AZ.properties index 151dc0e64..2304a13d1 100644 --- a/app/core/src/main/resources/messages_az_AZ.properties +++ b/app/core/src/main/resources/messages_az_AZ.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populyar settings.title=Parametrlər settings.update=Yeniləmə mövcuddur settings.updateAvailable={0} cari quraşdırılmış versiyadır. Yeni ({1}) versiyası mövcuddur. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Proqram Versiyası: settings.downloadOption.title=Yükləmə versiyasını seçin (Tək fayllı zip olmayan yükləmələr üçün): settings.downloadOption.1=Eyni pəncərədə açın diff --git a/app/core/src/main/resources/messages_bg_BG.properties b/app/core/src/main/resources/messages_bg_BG.properties index 63b0c0b85..a99e9447e 100644 --- a/app/core/src/main/resources/messages_bg_BG.properties +++ b/app/core/src/main/resources/messages_bg_BG.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярни settings.title=Настройки settings.update=Налична актуализация settings.updateAvailable={0} е текущата инсталирана версия. Налична е нова версия ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версия на приложението: settings.downloadOption.title=Изберете опция за изтегляне (за изтегляния на един файл без да е архивиран): settings.downloadOption.1=Отваряне в същия прозорец diff --git a/app/core/src/main/resources/messages_bo_CN.properties b/app/core/src/main/resources/messages_bo_CN.properties index 5b39cdcf5..aef66f128 100644 --- a/app/core/src/main/resources/messages_bo_CN.properties +++ b/app/core/src/main/resources/messages_bo_CN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=སྤྱི་མོས། settings.title=སྒྲིག་འགོད། settings.update=གསར་སྒྱུར་ཡོད། settings.updateAvailable={0} ནི་ད་ལྟ་སྒྲིག་འཇུག་བྱས་པའི་པར་གཞི་ཡིན། པར་གཞི་གསར་པ་ ({1}) ཡོད། + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=མཉེན་ཆས་པར་གཞི། settings.downloadOption.title=ཕབ་ལེན་གདམ་ག་འདེམས་རོགས། (ཡིག་ཆ་རྐྱང་པ་ zip མིན་པའི་ཕབ་ལེན་ཆེད།): settings.downloadOption.1=སྒེའུ་ཁུང་གཅིག་པའི་ནང་ཁ་ཕྱེ། diff --git a/app/core/src/main/resources/messages_ca_CA.properties b/app/core/src/main/resources/messages_ca_CA.properties index a8f9a560f..ff7f2b64b 100644 --- a/app/core/src/main/resources/messages_ca_CA.properties +++ b/app/core/src/main/resources/messages_ca_CA.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Opcions settings.update=Actualització Disponible settings.updateAvailable=La versió actual instal·lada és {0}. Una nova versió ({1}) està disponible. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versió de l'App: settings.downloadOption.title=Trieu l'opció de descàrrega (per a descàrregues d'un sol fitxer no comprimit): settings.downloadOption.1=Obre en la mateixa finestra diff --git a/app/core/src/main/resources/messages_cs_CZ.properties b/app/core/src/main/resources/messages_cs_CZ.properties index a83268aa2..a68fbcb78 100644 --- a/app/core/src/main/resources/messages_cs_CZ.properties +++ b/app/core/src/main/resources/messages_cs_CZ.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Oblíbené settings.title=Nastavení settings.update=K dispozici je aktualizace settings.updateAvailable={0} je aktuálně nainstalovaná verze. Je k dispozici nová verze ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verze aplikace: settings.downloadOption.title=Vyberte možnost stahování (Pro stahování jednoho souboru bez zipu): settings.downloadOption.1=Otevřít ve stejném okně diff --git a/app/core/src/main/resources/messages_da_DK.properties b/app/core/src/main/resources/messages_da_DK.properties index bc06c0915..8d55cc8d1 100644 --- a/app/core/src/main/resources/messages_da_DK.properties +++ b/app/core/src/main/resources/messages_da_DK.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populære settings.title=Indstillinger settings.update=Opdatering tilgængelig settings.updateAvailable={0} er den aktuelt installerede version. En ny version ({1}) er tilgængelig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Vælg download mulighed (For enkelt fil ikke-zip downloads): settings.downloadOption.1=Åbn i samme vindue diff --git a/app/core/src/main/resources/messages_de_DE.properties b/app/core/src/main/resources/messages_de_DE.properties index 1bb923450..63b54fa74 100644 --- a/app/core/src/main/resources/messages_de_DE.properties +++ b/app/core/src/main/resources/messages_de_DE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Beliebt settings.title=Einstellungen settings.update=Update verfügbar settings.updateAvailable={0} ist die aktuelle installierte Version. Eine neue Version ({1}) ist verfügbar. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App-Version: settings.downloadOption.title=Download-Option wählen (für einzelne Dateien, die keine Zip-Downloads sind): settings.downloadOption.1=Im selben Fenster öffnen diff --git a/app/core/src/main/resources/messages_el_GR.properties b/app/core/src/main/resources/messages_el_GR.properties index e4209faf8..a9fbee538 100644 --- a/app/core/src/main/resources/messages_el_GR.properties +++ b/app/core/src/main/resources/messages_el_GR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Δημοφιλή settings.title=Ρυθμίσεις settings.update=Διαθέσιμη ενημέρωση settings.updateAvailable={0} είναι η τρέχουσα εγκατεστημένη έκδοση. Μια νέα έκδοση ({1}) είναι διαθέσιμη. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Έκδοση εφαρμογής: settings.downloadOption.title=Επιλογή λήψης (Για μεμονωμένη λήψη αρχείων χωρίς συμπίεση): settings.downloadOption.1=Άνοιγμα στο ίδιο παράθυρο diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index e6bad97d0..877c25e75 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Settings settings.update=Update available settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Version: settings.downloadOption.title=Choose download option (For single file non zip downloads): settings.downloadOption.1=Open in same window diff --git a/app/core/src/main/resources/messages_es_ES.properties b/app/core/src/main/resources/messages_es_ES.properties index 40fe58987..4ccb6d758 100644 --- a/app/core/src/main/resources/messages_es_ES.properties +++ b/app/core/src/main/resources/messages_es_ES.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populares settings.title=Configuración settings.update=Actualización disponible settings.updateAvailable={0} es la versión instalada. Hay disponible una versión nueva ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versión de la aplicación: settings.downloadOption.title=Elegir la opción de descarga (para descargas de un solo archivo sin ZIP): settings.downloadOption.1=Abrir en la misma ventana diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 92bb97c63..513f5241e 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Ezarpenak settings.update=Eguneratze eskuragarria settings.updateAvailable={0} is the current installed version. A new version ({1}) is available. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Aplikazioaren bertsioa: settings.downloadOption.title=Hautatu deskargatzeko aukera (fitxategi bakarra deskargatzeko ZIP gabe): settings.downloadOption.1=Ireki leiho berean diff --git a/app/core/src/main/resources/messages_fa_IR.properties b/app/core/src/main/resources/messages_fa_IR.properties index 02e44b563..dccb7fc0b 100644 --- a/app/core/src/main/resources/messages_fa_IR.properties +++ b/app/core/src/main/resources/messages_fa_IR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=محبوب settings.title=تنظیمات settings.update=به‌روزرسانی موجود است settings.updateAvailable={0} نسخه نصب شده فعلی است. یک نسخه جدید ({1}) موجود است. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=نسخه برنامه: settings.downloadOption.title=گزینه دانلود را انتخاب کنید (برای دانلود یک فایل غیر فشرده): settings.downloadOption.1=باز کردن در همان پنجره diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index f45f94078..7f53edbfe 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populaire settings.title=Paramètres settings.update=Mise à jour disponible settings.updateAvailable={0} est la version actuellement installée. Une nouvelle version ({1}) est disponible. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Version de l'application : settings.downloadOption.title=Choisissez l'option de téléchargement (pour les téléchargements à fichier unique non ZIP) : settings.downloadOption.1=Ouvrir dans la même fenêtre diff --git a/app/core/src/main/resources/messages_ga_IE.properties b/app/core/src/main/resources/messages_ga_IE.properties index 874c8ebca..816932ff1 100644 --- a/app/core/src/main/resources/messages_ga_IE.properties +++ b/app/core/src/main/resources/messages_ga_IE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Coitianta settings.title=Socruithe settings.update=Nuashonrú ar fáil settings.updateAvailable=Is é {0} an leagan suiteáilte reatha. Tá leagan nua ({1}) ar fáil. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Leagan Aipe: settings.downloadOption.title=Roghnaigh rogha íoslódála (Le haghaidh íoslódálacha comhad amháin seachas zip): settings.downloadOption.1=Oscail sa bhfuinneog chéanna diff --git a/app/core/src/main/resources/messages_hi_IN.properties b/app/core/src/main/resources/messages_hi_IN.properties index 369d9444c..e2f9b2c19 100644 --- a/app/core/src/main/resources/messages_hi_IN.properties +++ b/app/core/src/main/resources/messages_hi_IN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=लोकप्रिय settings.title=सेटिंग्स settings.update=अपडेट उपलब्ध है settings.updateAvailable={0} वर्तमान स्थापित संस्करण है। एक नया संस्करण ({1}) उपलब्ध है। + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=ऐप संस्करण: settings.downloadOption.title=डाउनलोड विकल्प चुनें (एकल फ़ाइल गैर-ज़िप डाउनलोड के लिए): settings.downloadOption.1=उसी विंडो में खोलें diff --git a/app/core/src/main/resources/messages_hr_HR.properties b/app/core/src/main/resources/messages_hr_HR.properties index 87a4add1d..7ea02b909 100644 --- a/app/core/src/main/resources/messages_hr_HR.properties +++ b/app/core/src/main/resources/messages_hr_HR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularno settings.title=Postavke settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Dostupna je nova verzija ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzija aplikacije: settings.downloadOption.title=Odaberite opciju preuzimanja (Za preuzimanje pojedinačnih datoteka bez zip formata): settings.downloadOption.1=Otvori u istom prozoru diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 490dbecce..45de2334c 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Népszerű settings.title=Beállítások settings.update=Frissítés elérhető settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Alkalmazás verziója: settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén): settings.downloadOption.1=Megnyitás ugyanabban az ablakban diff --git a/app/core/src/main/resources/messages_id_ID.properties b/app/core/src/main/resources/messages_id_ID.properties index 470945372..541226f69 100644 --- a/app/core/src/main/resources/messages_id_ID.properties +++ b/app/core/src/main/resources/messages_id_ID.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populer settings.title=Pengaturan settings.update=Pembaruan tersedia settings.updateAvailable={0} adalah versi yang terpasang saat ini. Versi baru ({1}) tersedia. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versi Aplikasi: settings.downloadOption.title=Pilih opsi unduhan (Untuk unduhan berkas tunggal non zip): settings.downloadOption.1=Buka di jendela yang sama diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 71c0f9ffc..0db465a40 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popolare settings.title=Impostazioni settings.update=Aggiornamento disponibile settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versione App: settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi): settings.downloadOption.1=Apri in questa finestra diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index fdffa3523..ced0c7c56 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -366,6 +366,38 @@ navbar.sections.popular=人気 settings.title=設定 settings.update=利用可能なアップデート settings.updateAvailable=バージョン {0} がインストールされています。 新しいバージョン ({1}) が利用可能です。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Appバージョン: settings.downloadOption.title=ダウンロードオプション(zip以外の単一ファイル): settings.downloadOption.1=同じウィンドウで開く diff --git a/app/core/src/main/resources/messages_ko_KR.properties b/app/core/src/main/resources/messages_ko_KR.properties index b129e9c69..7de79d52c 100644 --- a/app/core/src/main/resources/messages_ko_KR.properties +++ b/app/core/src/main/resources/messages_ko_KR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=인기 settings.title=설정 settings.update=업데이트 가능 settings.updateAvailable={0}은(는) 현재 설치된 버전입니다. 새 버전({1})이 사용 가능합니다. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=앱 버전: settings.downloadOption.title=다운로드 옵션 선택 (단일 파일 비압축 다운로드용): settings.downloadOption.1=같은 창에서 열기 diff --git a/app/core/src/main/resources/messages_ml_IN.properties b/app/core/src/main/resources/messages_ml_IN.properties index 775b68792..123f5a53f 100644 --- a/app/core/src/main/resources/messages_ml_IN.properties +++ b/app/core/src/main/resources/messages_ml_IN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=ജനപ്രിയം settings.title=ക്രമീകരണങ്ങൾ settings.update=അപ്ഡേറ്റ് ലഭ്യമാണ് settings.updateAvailable={0} നിലവിൽ ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പാണ്. ഒരു പുതിയ പതിപ്പ് ({1}) ലഭ്യമാണ്. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=ആപ്പ് പതിപ്പ്: settings.downloadOption.title=ഡൗൺലോഡ് ഓപ്ഷൻ തിരഞ്ഞെടുക്കുക (സിംഗിൾ ഫയൽ നോൺ-സിപ്പ് ഡൗൺലോഡുകൾക്ക്): settings.downloadOption.1=ഒരേ വിൻഡോയിൽ തുറക്കുക diff --git a/app/core/src/main/resources/messages_nl_NL.properties b/app/core/src/main/resources/messages_nl_NL.properties index 94b1bb020..44418eb0f 100644 --- a/app/core/src/main/resources/messages_nl_NL.properties +++ b/app/core/src/main/resources/messages_nl_NL.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Instellingen settings.update=Update beschikbaar settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App versie: settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip): settings.downloadOption.1=Open in hetzelfde venster diff --git a/app/core/src/main/resources/messages_no_NB.properties b/app/core/src/main/resources/messages_no_NB.properties index dadc0bc32..ed830ec3f 100644 --- a/app/core/src/main/resources/messages_no_NB.properties +++ b/app/core/src/main/resources/messages_no_NB.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populært settings.title=Innstillinger settings.update=Oppdatering tilgjengelig settings.updateAvailable={0} er den nåværende installerte versjonen. En ny versjon ({1}) er tilgjengelig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=App Versjon: settings.downloadOption.title=Velg nedlastingsalternativ (For enkeltfil ikke-zip nedlastinger): settings.downloadOption.1=Åpne i samme vindu diff --git a/app/core/src/main/resources/messages_pl_PL.properties b/app/core/src/main/resources/messages_pl_PL.properties index 7d553c574..0eefb4ccc 100644 --- a/app/core/src/main/resources/messages_pl_PL.properties +++ b/app/core/src/main/resources/messages_pl_PL.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularne settings.title=Ustawienia settings.update=Dostępna aktualizacja settings.updateAvailable=Wersja {0} jest obecenia zainstalowana, dostępna jest nowa wersja ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Wersja aplikacji: settings.downloadOption.title=Wybierz opcję pobierania (w przypadku pobierania pojedynczych plików innych niż ZIP): settings.downloadOption.1=Otwórz w tym samym oknie diff --git a/app/core/src/main/resources/messages_pt_BR.properties b/app/core/src/main/resources/messages_pt_BR.properties index cde839e5e..57e8dd93e 100644 --- a/app/core/src/main/resources/messages_pt_BR.properties +++ b/app/core/src/main/resources/messages_pt_BR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populares settings.title=Configurações settings.update=Atualização disponível settings.updateAvailable={0} é a versão atualmente instalada. Uma nova versão ({1}) está disponível. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versão do Aplicativo: settings.downloadOption.title=Escolha a opção de download (para download de arquivo único, não compactados): settings.downloadOption.1=Abrir na mesma janela diff --git a/app/core/src/main/resources/messages_pt_PT.properties b/app/core/src/main/resources/messages_pt_PT.properties index 49998f273..2c78fa93b 100644 --- a/app/core/src/main/resources/messages_pt_PT.properties +++ b/app/core/src/main/resources/messages_pt_PT.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Definições settings.update=Atualização disponível settings.updateAvailable={0} é a versão atual instalada. Uma nova versão ({1}) está disponível. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versão da Aplicação: settings.downloadOption.title=Escolha a opção de download (Para downloads de ficheiro único não zipado): settings.downloadOption.1=Abrir na mesma janela diff --git a/app/core/src/main/resources/messages_ro_RO.properties b/app/core/src/main/resources/messages_ro_RO.properties index e33d01f4a..5a904a9c8 100644 --- a/app/core/src/main/resources/messages_ro_RO.properties +++ b/app/core/src/main/resources/messages_ro_RO.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Setări settings.update=Actualizare disponibilă settings.updateAvailable={0} este versiunea instalată curent. O nouă versiune ({1}) este disponibilă. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Versiune aplicație: settings.downloadOption.title=Alege opțiunea de descărcare (pentru descărcarea unui singur fișier non-zip): settings.downloadOption.1=Deschide în aceeași fereastră diff --git a/app/core/src/main/resources/messages_ru_RU.properties b/app/core/src/main/resources/messages_ru_RU.properties index 072e03123..4580f3933 100644 --- a/app/core/src/main/resources/messages_ru_RU.properties +++ b/app/core/src/main/resources/messages_ru_RU.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярное settings.title=Настройки settings.update=Доступно обновление settings.updateAvailable=Текущая установленная версия - {0}. Доступна новая версия ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версия приложения: settings.downloadOption.title=Выберите вариант загрузки (для одиночных файлов без архивации): settings.downloadOption.1=Открыть в том же окне diff --git a/app/core/src/main/resources/messages_sk_SK.properties b/app/core/src/main/resources/messages_sk_SK.properties index 10ed3d985..68faeab85 100644 --- a/app/core/src/main/resources/messages_sk_SK.properties +++ b/app/core/src/main/resources/messages_sk_SK.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Nastavenia settings.update=Dostupná aktualizácia settings.updateAvailable={0} je aktuálne nainštalovaná verzia. Nová verzia ({1}) je dostupná. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzia aplikácie: settings.downloadOption.title=Vyberte možnosť sťahovania (Pre jednotlivé neskomprimované súbory): settings.downloadOption.1=Otvoriť v rovnakom okne diff --git a/app/core/src/main/resources/messages_sl_SI.properties b/app/core/src/main/resources/messages_sl_SI.properties index 8b15dcc42..fe95a4165 100644 --- a/app/core/src/main/resources/messages_sl_SI.properties +++ b/app/core/src/main/resources/messages_sl_SI.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Priljubljeno settings.title=Nastavitve settings.update=Na voljo je posodobitev settings.updateAvailable={0} je trenutno nameščena različica. Na voljo je nova različica ({1}). + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Različica aplikacije: settings.downloadOption.title=Izberi možnost prenosa (za prenose ene datoteke brez zip): settings.downloadOption.1=Odpri v istem oknu diff --git a/app/core/src/main/resources/messages_sr_LATN_RS.properties b/app/core/src/main/resources/messages_sr_LATN_RS.properties index 305b68aa1..f15d8397a 100644 --- a/app/core/src/main/resources/messages_sr_LATN_RS.properties +++ b/app/core/src/main/resources/messages_sr_LATN_RS.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popularno settings.title=Podešavanja settings.update=Dostupno ažuriranje settings.updateAvailable={0} je trenutno instalirana verzija. Nova verzija ({1}) je dostupna. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Verzija aplikacije: settings.downloadOption.title=Odaberi opciju preuzimanja (Za preuzimanje pojedinačnih fajlova bez zip formata): settings.downloadOption.1=Otvori u istom prozoru diff --git a/app/core/src/main/resources/messages_sv_SE.properties b/app/core/src/main/resources/messages_sv_SE.properties index e731f6337..7a786add6 100644 --- a/app/core/src/main/resources/messages_sv_SE.properties +++ b/app/core/src/main/resources/messages_sv_SE.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Populära settings.title=Inställningar settings.update=Uppdatering tillgänglig settings.updateAvailable={0} är den aktuella installerade versionen. En ny version ({1}) finns tillgänglig. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Appversion: settings.downloadOption.title=Välj nedladdningsalternativ (för nedladdning av en fil utan zip): settings.downloadOption.1=Öppnas i samma fönster diff --git a/app/core/src/main/resources/messages_th_TH.properties b/app/core/src/main/resources/messages_th_TH.properties index 7a2b20aea..9b332982c 100644 --- a/app/core/src/main/resources/messages_th_TH.properties +++ b/app/core/src/main/resources/messages_th_TH.properties @@ -366,6 +366,38 @@ navbar.sections.popular=ยอดนิยม settings.title=การตั้งค่า settings.update=มีการอัปเดต settings.updateAvailable={0} คือเวอร์ชันที่ติดตั้งในปัจจุบัน มีเวอร์ชันใหม่ ({1}) พร้อมให้บริการ + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=เวอร์ชันแอป: settings.downloadOption.title=เลือกตัวเลือกการดาวน์โหลด (สำหรับการดาวน์โหลดไฟล์เดียวที่ไม่ใช่ zip): settings.downloadOption.1=เปิดในหน้าต่างเดียวกัน diff --git a/app/core/src/main/resources/messages_tr_TR.properties b/app/core/src/main/resources/messages_tr_TR.properties index c03d7872e..72e78f1b3 100644 --- a/app/core/src/main/resources/messages_tr_TR.properties +++ b/app/core/src/main/resources/messages_tr_TR.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Ayarlar settings.update=Güncelleme mevcut settings.updateAvailable={0} mevcut kurulu sürümdür. Yeni bir sürüm ({1}) mevcuttur. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Uygulama Sürümü: settings.downloadOption.title=İndirme seçeneği seçin (Zip olmayan tek dosya indirmeler için): settings.downloadOption.1=Aynı pencerede aç diff --git a/app/core/src/main/resources/messages_uk_UA.properties b/app/core/src/main/resources/messages_uk_UA.properties index f24d997ac..db5739fe3 100644 --- a/app/core/src/main/resources/messages_uk_UA.properties +++ b/app/core/src/main/resources/messages_uk_UA.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Популярне settings.title=Налаштування settings.update=Доступне оновлення settings.updateAvailable=Зараз встановлена версія {0}. Нова версія ({1}) доступна. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Версія додатку: settings.downloadOption.title=Виберіть варіант завантаження (для завантаження одного файлу без zip): settings.downloadOption.1=Відкрити в тому ж вікні diff --git a/app/core/src/main/resources/messages_vi_VN.properties b/app/core/src/main/resources/messages_vi_VN.properties index cd2e412f7..0a1e9b392 100644 --- a/app/core/src/main/resources/messages_vi_VN.properties +++ b/app/core/src/main/resources/messages_vi_VN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=Popular settings.title=Cài đặt settings.update=Có bản cập nhật settings.updateAvailable={0} là phiên bản hiện tại đã cài đặt. Một phiên bản mới ({1}) đã có sẵn. + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=Phiên bản ứng dụng: settings.downloadOption.title=Chọn tùy chọn tải xuống (Đối với tải xuống tệp đơn không nén): settings.downloadOption.1=Mở trong cùng cửa sổ diff --git a/app/core/src/main/resources/messages_zh_CN.properties b/app/core/src/main/resources/messages_zh_CN.properties index 252eb2768..4eeac6483 100644 --- a/app/core/src/main/resources/messages_zh_CN.properties +++ b/app/core/src/main/resources/messages_zh_CN.properties @@ -366,6 +366,38 @@ navbar.sections.popular=热门 settings.title=设置 settings.update=有可用的更新 settings.updateAvailable=当前版本为 {0},新版本 ({1}) 可用。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=应用程序版本: settings.downloadOption.title=选择下载选项(单个文件非压缩文件): settings.downloadOption.1=在同一窗口打开 diff --git a/app/core/src/main/resources/messages_zh_TW.properties b/app/core/src/main/resources/messages_zh_TW.properties index 9f38178ac..cee6b9c7d 100644 --- a/app/core/src/main/resources/messages_zh_TW.properties +++ b/app/core/src/main/resources/messages_zh_TW.properties @@ -366,6 +366,38 @@ navbar.sections.popular=熱門功能 settings.title=設定 settings.update=有更新可用 settings.updateAvailable=目前安裝的版本是 {0}。有新版本({1})可供使用。 + +# Update modal and notification strings +update.urgentUpdateAvailable=🚨 Update Available +update.updateAvailable=Update Available +update.modalTitle=Update Available +update.current=Current +update.latest=Latest +update.latestStable=Latest Stable +update.priority=Priority +update.recommendedAction=Recommended Action +update.breakingChangesDetected=⚠️ Breaking Changes Detected +update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. +update.migrationGuides=Migration Guides: +update.viewGuide=View Guide +update.loadingDetailedInfo=Loading detailed version information... +update.close=Close +update.viewAllReleases=View All Releases +update.downloadLatest=Download Latest +update.availableUpdates=Available Updates: +update.unableToLoadDetails=Unable to load detailed version information. +update.version=Version + +# Update priority levels +update.priority.urgent=URGENT +update.priority.normal=NORMAL +update.priority.minor=MINOR +update.priority.low=LOW + +# Breaking changes text +update.breakingChanges=Breaking Changes: +update.breakingChangesDefault=This version contains breaking changes +update.migrationGuide=Migration Guide settings.appVersion=應用程式版本: settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載): settings.downloadOption.1=在同一視窗中開啟 From 71ac4283b27f727cb2d61e5e999ff7daadf6f5fa Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:39:47 +0100 Subject: [PATCH 14/23] PSD (#4146) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../common/util/ImageProcessingUtils.java | 32 ++++++++++++++++++- app/core/build.gradle | 2 +- .../main/resources/messages_en_GB.properties | 8 ++--- .../main/resources/messages_en_US.properties | 8 ++--- .../main/resources/messages_eu_ES.properties | 4 +-- .../main/resources/messages_fr_FR.properties | 2 +- .../main/resources/messages_ja_JP.properties | 4 +-- .../templates/convert/img-to-pdf.html | 2 +- build.gradle | 2 +- 9 files changed, 47 insertions(+), 17 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index ae6c0b66f..fd4091d4c 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -5,8 +5,11 @@ import java.awt.image.*; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.Iterator; import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; import org.springframework.web.multipart.MultipartFile; @@ -115,7 +118,34 @@ public class ImageProcessingUtils { public static BufferedImage loadImageWithExifOrientation(MultipartFile file) throws IOException { - BufferedImage image = ImageIO.read(file.getInputStream()); + BufferedImage image = null; + String filename = file.getOriginalFilename(); + + if (filename != null && filename.toLowerCase().endsWith(".psd")) { + // For PSD files, try explicit ImageReader + Iterator readers = ImageIO.getImageReadersByFormatName("PSD"); + if (readers.hasNext()) { + ImageReader reader = readers.next(); + try (ImageInputStream iis = ImageIO.createImageInputStream(file.getInputStream())) { + reader.setInput(iis); + image = reader.read(0); + } finally { + reader.dispose(); + } + } + if (image == null) { + throw new IOException("Unable to read image from file: " + filename + + ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); + } + } else { + // For non-PSD files, use standard ImageIO + image = ImageIO.read(file.getInputStream()); + } + + if (image == null) { + throw new IOException("Unable to read image from file: " + filename); + } + double orientation = extractImageOrientation(file.getInputStream()); return applyOrientation(image, orientation); } diff --git a/app/core/build.gradle b/app/core/build.gradle index 037a89497..c9905a308 100644 --- a/app/core/build.gradle +++ b/app/core/build.gradle @@ -91,7 +91,7 @@ dependencies { // runtimeOnly "com.twelvemonkeys.imageio:imageio-pcx:$imageioVersion@ // runtimeOnly "com.twelvemonkeys.imageio:imageio-pict:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-pnm:$imageioVersion" - // runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion" + runtimeOnly "com.twelvemonkeys.imageio:imageio-psd:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-sgi:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-tga:$imageioVersion" // runtimeOnly "com.twelvemonkeys.imageio:imageio-thumbsdb:$imageioVersion" diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index f619b7b6e..d6056e856 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -601,12 +601,12 @@ rotate.tags=server side home.imageToPdf.title=Image to PDF -home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF. -imageToPdf.tags=conversion,img,jpg,picture,photo +home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDF to Image -home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) -pdfToImage.tags=conversion,img,jpg,picture,photo +home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Organise home.pdfOrganiser.desc=Remove/Rearrange pages in any order diff --git a/app/core/src/main/resources/messages_en_US.properties b/app/core/src/main/resources/messages_en_US.properties index 877c25e75..250dd51c5 100644 --- a/app/core/src/main/resources/messages_en_US.properties +++ b/app/core/src/main/resources/messages_en_US.properties @@ -601,12 +601,12 @@ rotate.tags=server side home.imageToPdf.title=Image to PDF -home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF) to PDF. -imageToPdf.tags=conversion,img,jpg,picture,photo +home.imageToPdf.desc=Convert a image (PNG, JPEG, GIF, PSD) to PDF. +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDF to Image -home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF) -pdfToImage.tags=conversion,img,jpg,picture,photo +home.pdfToImage.desc=Convert a PDF to a image. (PNG, JPEG, GIF, PSD) +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Organize home.pdfOrganiser.desc=Remove/Rearrange pages in any order diff --git a/app/core/src/main/resources/messages_eu_ES.properties b/app/core/src/main/resources/messages_eu_ES.properties index 513f5241e..27dbfdb08 100644 --- a/app/core/src/main/resources/messages_eu_ES.properties +++ b/app/core/src/main/resources/messages_eu_ES.properties @@ -602,11 +602,11 @@ rotate.tags=server side home.imageToPdf.title=Irudia PDF bihurtu home.imageToPdf.desc=Irudi bat(PNG, JPEG, GIF)PDF bihurtu -imageToPdf.tags=conversion,img,jpg,picture,photo +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDFa irudi bihurtu home.pdfToImage.desc=PDF bat irudi (PNG, JPEG, GIF) bihurtu -pdfToImage.tags=conversion,img,jpg,picture,photo +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=Antolatzailea home.pdfOrganiser.desc=Ezabatu/Berrantolatu orrialdeak edozein ordenatan diff --git a/app/core/src/main/resources/messages_fr_FR.properties b/app/core/src/main/resources/messages_fr_FR.properties index 7f53edbfe..86e6c0d95 100644 --- a/app/core/src/main/resources/messages_fr_FR.properties +++ b/app/core/src/main/resources/messages_fr_FR.properties @@ -601,7 +601,7 @@ rotate.tags=pivoter,server side,rotate home.imageToPdf.title=Image en PDF -home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF) en PDF. +home.imageToPdf.desc=Convertissez une image (PNG, JPEG, GIF, PSD) en PDF. imageToPdf.tags=pdf,conversion,img,jpg,image,photo home.pdfToImage.title=PDF en image diff --git a/app/core/src/main/resources/messages_ja_JP.properties b/app/core/src/main/resources/messages_ja_JP.properties index ced0c7c56..a5af895fd 100644 --- a/app/core/src/main/resources/messages_ja_JP.properties +++ b/app/core/src/main/resources/messages_ja_JP.properties @@ -602,11 +602,11 @@ rotate.tags=server side home.imageToPdf.title=画像をPDFに変換 home.imageToPdf.desc=画像 (PNG, JPEG, GIF) をPDFに変換します。 -imageToPdf.tags=conversion,img,jpg,picture,photo +imageToPdf.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfToImage.title=PDFを画像に変換 home.pdfToImage.desc=PDFを画像 (PNG, JPEG, GIF) に変換します。 -pdfToImage.tags=conversion,img,jpg,picture,photo +pdfToImage.tags=conversion,img,jpg,picture,photo,psd,photoshop home.pdfOrganiser.title=整理 home.pdfOrganiser.desc=ページの削除/並べ替えします。 diff --git a/app/core/src/main/resources/templates/convert/img-to-pdf.html b/app/core/src/main/resources/templates/convert/img-to-pdf.html index 6c37e6473..c3b01eec2 100644 --- a/app/core/src/main/resources/templates/convert/img-to-pdf.html +++ b/app/core/src/main/resources/templates/convert/img-to-pdf.html @@ -22,7 +22,7 @@
+ th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='image/*,.psd', inputText=#{imgPrompt})}">
diff --git a/build.gradle b/build.gradle index 39672cf24..e54c58e7d 100644 --- a/build.gradle +++ b/build.gradle @@ -58,7 +58,7 @@ repositories { allprojects { group = 'stirling.software' - version = '1.1.2' + version = '1.2.0' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' From 678a9bc4636a2589e7fc32a7d23d08c852b391c0 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:53:45 +0100 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=A4=96=20format=20everything=20with?= =?UTF-8?q?=20pre-commit=20by=20stirlingbot=20(#4150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-generated by [create-pull-request][1] with **stirlingbot** [1]: https://github.com/peter-evans/create-pull-request Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../software/common/util/ImageProcessingUtils.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java index fd4091d4c..7140b3cc2 100644 --- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java @@ -120,7 +120,7 @@ public class ImageProcessingUtils { throws IOException { BufferedImage image = null; String filename = file.getOriginalFilename(); - + if (filename != null && filename.toLowerCase().endsWith(".psd")) { // For PSD files, try explicit ImageReader Iterator readers = ImageIO.getImageReadersByFormatName("PSD"); @@ -134,18 +134,20 @@ public class ImageProcessingUtils { } } if (image == null) { - throw new IOException("Unable to read image from file: " + filename + - ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); + throw new IOException( + "Unable to read image from file: " + + filename + + ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression"); } } else { // For non-PSD files, use standard ImageIO image = ImageIO.read(file.getInputStream()); } - + if (image == null) { throw new IOException("Unable to read image from file: " + filename); } - + double orientation = extractImageOrientation(file.getInputStream()); return applyOrientation(image, orientation); } From 796873134f1c3c37fbfcdf7d6375fc21481be7aa Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:54:00 +0100 Subject: [PATCH 16/23] Update 3rd Party Licenses (#4122) Auto-generated by stirlingbot[bot] Signed-off-by: stirlingbot[bot] Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- .../main/resources/static/3rdPartyLicenses.json | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/core/src/main/resources/static/3rdPartyLicenses.json b/app/core/src/main/resources/static/3rdPartyLicenses.json index 59acd0fc2..23278a23f 100644 --- a/app/core/src/main/resources/static/3rdPartyLicenses.json +++ b/app/core/src/main/resources/static/3rdPartyLicenses.json @@ -336,6 +336,12 @@ "moduleLicense": "The BSD License", "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" }, + { + "moduleName": "com.twelvemonkeys.imageio:imageio-psd", + "moduleVersion": "3.12.0", + "moduleLicense": "The BSD License", + "moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license" + }, { "moduleName": "com.twelvemonkeys.imageio:imageio-tiff", "moduleVersion": "3.12.0", @@ -623,21 +629,21 @@ { "moduleName": "io.swagger.core.v3:swagger-annotations-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-annotations", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-core-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-core", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "io.swagger.core.v3:swagger-models-jakarta", "moduleUrl": "https://github.com/swagger-api/swagger-core/modules/swagger-models", - "moduleVersion": "2.2.34", + "moduleVersion": "2.2.35", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -980,13 +986,13 @@ }, { "moduleName": "org.commonmark:commonmark", - "moduleVersion": "0.25.0", + "moduleVersion": "0.25.1", "moduleLicense": "BSD-2-Clause", "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" }, { "moduleName": "org.commonmark:commonmark-ext-gfm-tables", - "moduleVersion": "0.25.0", + "moduleVersion": "0.25.1", "moduleLicense": "BSD-2-Clause", "moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause" }, From e8b5ae0474a8333a2eb141aa18e185d73f608317 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Sat, 9 Aug 2025 00:07:20 +0200 Subject: [PATCH 17/23] Update messages_it_IT.properties (#4154) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../main/resources/messages_it_IT.properties | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/core/src/main/resources/messages_it_IT.properties b/app/core/src/main/resources/messages_it_IT.properties index 0db465a40..74952b670 100644 --- a/app/core/src/main/resources/messages_it_IT.properties +++ b/app/core/src/main/resources/messages_it_IT.properties @@ -368,36 +368,36 @@ settings.update=Aggiornamento disponibile settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Aggiornamento disponibile +update.updateAvailable=Aggiornamento disponibile +update.modalTitle=Aggiornamento disponibile +update.current=Corrente +update.latest=Ultimo +update.latestStable=Ultima versione stabile +update.priority=Priorità +update.recommendedAction=Azione consigliata +update.breakingChangesDetected=⚠️ Rilevate modifiche sostanziali +update.breakingChangesMessage=Questo aggiornamento contiene modifiche sostanziali. Consulta le guide alla migrazione riportate di seguito. +update.migrationGuides=Guide alla migrazione: +update.viewGuide=Visualizza la guida +update.loadingDetailedInfo=Caricamento delle informazioni dettagliate sulla versione... +update.close=Chiudi +update.viewAllReleases=Visualizza tutte le versioni +update.downloadLatest=Scarica l'ultima +update.availableUpdates=Aggiornamenti disponibili: +update.unableToLoadDetails=Impossibile caricare informazioni dettagliate sulla versione. +update.version=Versione # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=URGENTE +update.priority.normal=NORMALE +update.priority.minor=MINORE +update.priority.low=BASSA # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Modifiche sostanziali: +update.breakingChangesDefault=Questa versione contiene modifiche sostanziali +update.migrationGuide=Guida alla migrazione settings.appVersion=Versione App: settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi): settings.downloadOption.1=Apri in questa finestra @@ -1696,7 +1696,7 @@ fileChooser.dragAndDrop=Trascina & Rilascia fileChooser.dragAndDropPDF=Trascina & rilascia il file PDF fileChooser.dragAndDropImage=Trascina & rilascia il file immagine fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui -fileChooser.extractPDF=Estraendo... +fileChooser.extractPDF=Estrazione... fileChooser.addAttachments=trascina & rilascia gli allegati qui #release notes @@ -1892,12 +1892,12 @@ editTableOfContents.replaceExisting=Sostituisci i segnalibri esistenti (deselezi editTableOfContents.editorTitle=Editor segnalibri editTableOfContents.editorDesc=Aggiungi e disponi i segnalibri qui sotto. Fai clic su + per aggiungere segnalibri secondari. editTableOfContents.addBookmark=Aggiungi nuovo segnalibro -editTableOfContents.importBookmarksDefault=Import -editTableOfContents.importBookmarksFromJsonFile=Upload JSON file -editTableOfContents.importBookmarksFromClipboard=Paste from clipboard -editTableOfContents.exportBookmarksDefault=Export -editTableOfContents.exportBookmarksAsJson=Download as JSON -editTableOfContents.exportBookmarksAsText=Copy as text +editTableOfContents.importBookmarksDefault=Importa +editTableOfContents.importBookmarksFromJsonFile=Carica file JSON +editTableOfContents.importBookmarksFromClipboard=Incolla dagli appunti +editTableOfContents.exportBookmarksDefault=Esporta +editTableOfContents.exportBookmarksAsJson=Scarica come JSON +editTableOfContents.exportBookmarksAsText=Copia come testo editTableOfContents.desc.1=Questo strumento consente di aggiungere o modificare il sommario (segnalibri) in un documento PDF. editTableOfContents.desc.2=È possibile creare una struttura gerarchica aggiungendo segnalibri secondari a quelli principali. editTableOfContents.desc.3=Ogni segnalibro richiede un titolo e un numero di pagina di destinazione. From 3938a07c132080cfb362145db0378803be3a58f8 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:13:33 +0100 Subject: [PATCH 18/23] :globe_with_meridians: Sync Translations + Update README Progress Table (#4155) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3b582cbfc..5e212780a 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Stirling-PDF currently supports 40 languages! | Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | | Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | | Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | -| Italian (Italiano) (it_IT) | ![96%](https://geps.dev/progress/96) | +| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) | | Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | | Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) | | Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) | From 5e01b15d3ca466abcae5a707aa6dc4c3de6f4e89 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 9 Aug 2025 12:03:24 +0100 Subject: [PATCH 19/23] Update .files.yaml for V2 (#4156) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/config/.files.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/config/.files.yaml b/.github/config/.files.yaml index 225470ea9..a5d8410f3 100644 --- a/.github/config/.files.yaml +++ b/.github/config/.files.yaml @@ -27,3 +27,5 @@ project: &project - gradlew.bat - launch4jConfig.xml - settings.gradle + - frontend/** + - docker/** From 299ce03dda733daf47a9da527dbc004fca3c5d34 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:09:26 +0100 Subject: [PATCH 20/23] Update CODEOWNERS (#4158) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .github/CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7d5389fda..f89c7154d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,20 +2,20 @@ * @Frooodle @Ludy87 @jbrunton96 @ConnorYoh # Backend -/app/** @DarioGii +/app/** @DarioGii @Frooodle @Ludy87 @jbrunton96 @ConnorYoh #V1 frontend -/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 -/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/app/core/src/main/resources/static/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 +/app/core/src/main/resources/templates/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 #V2 frontend -/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 +/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle #V2 docker -/docker/backend/** @Frooodle @Ludy87 @DarioGii -/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 -/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 +/docker/backend/** @Frooodle @Ludy87 @DarioGii @Ludy87 +/docker/frontend/** @reecebrowne @ConnorYoh @EthanHealy01 @jbrunton96 @Frooodle @Ludy87 +/docker/compose/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 #GHA (All users) -/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 +/.github/** @reecebrowne @ConnorYoh @EthanHealy01 @DarioGii @jbrunton96 @Frooodle @Ludy87 From 05b5771c89ccb9cb1b357d14fba21e2013420b12 Mon Sep 17 00:00:00 2001 From: Ludy Date: Sat, 9 Aug 2025 16:09:50 +0200 Subject: [PATCH 21/23] fix(saml): correct ClassPathResource handling for IdP metadata and add null-guard for privateKey (#4157) ## Description of Changes **What was changed** - In `getIdpMetadataUri()`, use `idpMetadataUri.substring("classpath:".length())` so the `classpath:` scheme (including the colon) is stripped correctly before creating the `ClassPathResource`. - In `getPrivateKey()`, add a null check (`if (privateKey == null) return null;`) to avoid a potential `NullPointerException` when the property is unset. **Why the change was made** - The previous substring used `"classpath".length()` (without the colon), leaving a leading `:` in the path (e.g., `:/saml/idp.xml`) which breaks `ClassPathResource` resolution and can prevent SAML bootstrapping when `idpMetadataUri` uses the `classpath:` scheme. - The null-guard aligns the method with defensive coding practices and prevents runtime errors when no private key is configured. --- ## 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/devGuide/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/devGuide/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) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../common/model/ApplicationProperties.java | 3 +- ...opertiesDynamicYamlPropertySourceTest.java | 59 +++++ .../model/ApplicationPropertiesLogicTest.java | 248 ++++++++++++++++++ .../ApplicationPropertiesSaml2HttpTest.java | 80 ++++++ ...pplicationPropertiesSaml2ResourceTest.java | 55 ++++ app/common/src/test/resources/saml/dummy.txt | 1 + build.gradle | 19 +- 7 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java create mode 100644 app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java create mode 100644 app/common/src/test/resources/saml/dummy.txt diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index fb93ef345..ee893c575 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -197,7 +197,7 @@ public class ApplicationProperties { @JsonIgnore public InputStream getIdpMetadataUri() throws IOException { if (idpMetadataUri.startsWith("classpath:")) { - return new ClassPathResource(idpMetadataUri.substring("classpath".length())) + return new ClassPathResource(idpMetadataUri.substring("classpath:".length())) .getInputStream(); } try { @@ -233,6 +233,7 @@ public class ApplicationProperties { @JsonIgnore public Resource getPrivateKey() { + if (privateKey == null) return null; if (privateKey.startsWith("classpath:")) { return new ClassPathResource(privateKey.substring("classpath:".length())); } else { diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java new file mode 100644 index 000000000..71d3997be --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesDynamicYamlPropertySourceTest.java @@ -0,0 +1,59 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.StandardEnvironment; + +import stirling.software.common.configuration.InstallationPathConfig; + +class ApplicationPropertiesDynamicYamlPropertySourceTest { + + @Test + void loads_yaml_into_environment() throws Exception { + // YAML-Config in Temp-Datei schreiben + String yaml = + "" + + "ui:\n" + + " appName: \"My App\"\n" + + "system:\n" + + " enableAnalytics: true\n"; + Path tmp = Files.createTempFile("spdf-settings-", ".yml"); + Files.writeString(tmp, yaml); + + // Pfad per statischem Mock liefern + try (MockedStatic mocked = + Mockito.mockStatic(InstallationPathConfig.class)) { + mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString()); + + ConfigurableEnvironment env = new StandardEnvironment(); + ApplicationProperties props = new ApplicationProperties(); + + props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein + + assertEquals("My App", env.getProperty("ui.appName")); + assertEquals("true", env.getProperty("system.enableAnalytics")); + } + } + + @Test + void throws_when_settings_file_missing() throws Exception { + String missing = "/path/does/not/exist/spdf.yml"; + try (MockedStatic mocked = + Mockito.mockStatic(InstallationPathConfig.class)) { + mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(missing); + + ConfigurableEnvironment env = new StandardEnvironment(); + ApplicationProperties props = new ApplicationProperties(); + + assertThrows(IOException.class, () -> props.dynamicYamlPropertySource(env)); + } + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java new file mode 100644 index 000000000..da83fd462 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesLogicTest.java @@ -0,0 +1,248 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import stirling.software.common.model.ApplicationProperties.Driver; +import stirling.software.common.model.ApplicationProperties.Premium; +import stirling.software.common.model.ApplicationProperties.Security; +import stirling.software.common.model.exception.UnsupportedProviderException; + +class ApplicationPropertiesLogicTest { + + @Test + void system_isAnalyticsEnabled_null_false_true() { + ApplicationProperties.System sys = new ApplicationProperties.System(); + + sys.setEnableAnalytics(null); + assertFalse(sys.isAnalyticsEnabled()); + + sys.setEnableAnalytics(Boolean.FALSE); + assertFalse(sys.isAnalyticsEnabled()); + + sys.setEnableAnalytics(Boolean.TRUE); + assertTrue(sys.isAnalyticsEnabled()); + } + + @Test + void tempFileManagement_defaults_and_overrides() { + ApplicationProperties.TempFileManagement tfm = + new ApplicationProperties.TempFileManagement(); + + String expectedBase = + java.lang.System.getProperty("java.io.tmpdir").replaceAll("/+$", "") + + "/stirling-pdf"; + assertEquals(expectedBase, tfm.getBaseTmpDir()); + + String expectedLibre = expectedBase + "/libreoffice"; + assertEquals(expectedLibre, tfm.getLibreofficeDir()); + + tfm.setBaseTmpDir("/custom/base"); + assertEquals("/custom/base", tfm.getBaseTmpDir()); + + tfm.setLibreofficeDir("/opt/libre"); + assertEquals("/opt/libre", tfm.getLibreofficeDir()); + } + + @Test + void oauth2_scope_parsing_and_validity() { + Security.OAUTH2 oauth2 = new Security.OAUTH2(); + oauth2.setIssuer("https://issuer"); + oauth2.setClientId("client"); + oauth2.setClientSecret("secret"); + oauth2.setUseAsUsername("email"); + oauth2.setScopes("openid, profile ,email"); + assertTrue(oauth2.isSettingsValid()); + } + + @Test + void security_login_method_flags() { + Security sec = new Security(); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(true); + + assertTrue(sec.isUserPass()); + assertTrue(sec.isOauth2Active()); + assertTrue(sec.isSaml2Active()); + + sec.setLoginMethod(Security.LoginMethods.NORMAL.toString()); + assertTrue(sec.isUserPass()); + assertFalse(sec.isOauth2Active()); + assertFalse(sec.isSaml2Active()); + } + + @Test + void security_isAltLogin_reflects_oauth2_or_saml2() { + Security sec = new Security(); + + assertFalse(sec.isAltLogin()); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(false); + assertTrue(sec.isAltLogin()); + + sec.getOauth2().setEnabled(false); + sec.getSaml2().setEnabled(true); + assertTrue(sec.isAltLogin()); + + sec.getOauth2().setEnabled(true); + sec.getSaml2().setEnabled(true); + assertTrue(sec.isAltLogin()); + } + + @Test + void oauth2_client_provider_mapping_and_unsupported() throws UnsupportedProviderException { + Security.OAUTH2.Client client = new Security.OAUTH2.Client(); + + assertNotNull(client.get("google")); + assertNotNull(client.get("github")); + assertNotNull(client.get("keycloak")); + + UnsupportedProviderException ex = + assertThrows(UnsupportedProviderException.class, () -> client.get("unknown")); + assertTrue(ex.getMessage().toLowerCase().contains("not supported")); + } + + @Test + void premium_google_drive_getters_return_empty_string_on_null_or_blank() { + Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive(); + + assertEquals("", gd.getClientId()); + assertEquals("", gd.getApiKey()); + assertEquals("", gd.getAppId()); + + gd.setClientId(" id "); + gd.setApiKey(" key "); + gd.setAppId(" app "); + assertEquals(" id ", gd.getClientId()); + assertEquals(" key ", gd.getApiKey()); + assertEquals(" app ", gd.getAppId()); + } + + @Test + void ui_getters_return_null_for_blank() { + ApplicationProperties.Ui ui = new ApplicationProperties.Ui(); + ui.setAppName(" "); + ui.setHomeDescription(""); + ui.setAppNameNavbar(null); + + assertNull(ui.getAppName()); + assertNull(ui.getHomeDescription()); + assertNull(ui.getAppNameNavbar()); + + ui.setAppName("Stirling-PDF"); + ui.setHomeDescription("Home"); + ui.setAppNameNavbar("Nav"); + assertEquals("Stirling-PDF", ui.getAppName()); + assertEquals("Home", ui.getHomeDescription()); + assertEquals("Nav", ui.getAppNameNavbar()); + } + + @Test + void driver_toString_contains_driver_name() { + assertTrue(Driver.H2.toString().contains("h2")); + assertTrue(Driver.POSTGRESQL.toString().contains("postgresql")); + } + + @Test + void session_limits_and_timeouts_have_reasonable_defaults() { + ApplicationProperties.ProcessExecutor pe = new ApplicationProperties.ProcessExecutor(); + + ApplicationProperties.ProcessExecutor.SessionLimit s = pe.getSessionLimit(); + assertEquals(2, s.getQpdfSessionLimit()); + assertEquals(1, s.getTesseractSessionLimit()); + assertEquals(1, s.getLibreOfficeSessionLimit()); + assertEquals(1, s.getPdfToHtmlSessionLimit()); + assertEquals(8, s.getPythonOpenCvSessionLimit()); + assertEquals(16, s.getWeasyPrintSessionLimit()); + assertEquals(1, s.getInstallAppSessionLimit()); + assertEquals(1, s.getCalibreSessionLimit()); + assertEquals(8, s.getGhostscriptSessionLimit()); + assertEquals(2, s.getOcrMyPdfSessionLimit()); + + ApplicationProperties.ProcessExecutor.TimeoutMinutes t = pe.getTimeoutMinutes(); + assertEquals(30, t.getTesseractTimeoutMinutes()); + assertEquals(30, t.getQpdfTimeoutMinutes()); + assertEquals(30, t.getLibreOfficeTimeoutMinutes()); + assertEquals(20, t.getPdfToHtmlTimeoutMinutes()); + assertEquals(30, t.getPythonOpenCvTimeoutMinutes()); + assertEquals(30, t.getWeasyPrintTimeoutMinutes()); + assertEquals(60, t.getInstallAppTimeoutMinutes()); + assertEquals(30, t.getCalibreTimeoutMinutes()); + assertEquals(30, t.getGhostscriptTimeoutMinutes()); + assertEquals(30, t.getOcrMyPdfTimeoutMinutes()); + } + + @Deprecated + @Test + void enterprise_metadata_defaults() { + ApplicationProperties.EnterpriseEdition ee = new ApplicationProperties.EnterpriseEdition(); + ApplicationProperties.EnterpriseEdition.CustomMetadata eMeta = ee.getCustomMetadata(); + eMeta.setCreator(" "); + eMeta.setProducer(null); + assertEquals("Stirling-PDF", eMeta.getCreator()); + assertEquals("Stirling-PDF", eMeta.getProducer()); + } + + @Test + void premium_metadata_defaults() { + Premium.ProFeatures pf = new Premium.ProFeatures(); + Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata(); + pMeta.setCreator(""); + pMeta.setProducer(""); + assertEquals("Stirling-PDF", pMeta.getCreator()); + assertEquals("Stirling-PDF", pMeta.getProducer()); + } + + @Test + void premium_metadata_awesome() { + Premium.ProFeatures pf = new Premium.ProFeatures(); + Premium.ProFeatures.CustomMetadata pMeta = pf.getCustomMetadata(); + pMeta.setCreator("Awesome PDF Tool"); + pMeta.setProducer("Awesome PDF Tool"); + assertEquals("Awesome PDF Tool", pMeta.getCreator()); + assertEquals("Awesome PDF Tool", pMeta.getProducer()); + } + + @Test + void string_isValid_handles_null_empty_blank_and_trimmed() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + assertFalse(oauth2.isValid((String) null, "issuer")); + assertFalse(oauth2.isValid("", "issuer")); + assertFalse(oauth2.isValid(" ", "issuer")); + + assertTrue(oauth2.isValid("x", "issuer")); + assertTrue(oauth2.isValid(" x ", "issuer")); // trimmt intern + } + + @Test + void collection_isValid_handles_null_and_empty() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + Collection nullColl = null; + Collection empty = List.of(); + + assertFalse(oauth2.isValid(nullColl, "scopes")); + assertFalse(oauth2.isValid(empty, "scopes")); + } + + @Test + void collection_isValid_true_when_non_empty_even_if_element_is_blank() { + ApplicationProperties.Security.OAUTH2 oauth2 = new ApplicationProperties.Security.OAUTH2(); + + // Aktuelles Verhalten: prüft NUR !isEmpty(), nicht Inhalt + Collection oneBlank = new ArrayList<>(); + oneBlank.add(" "); + + assertTrue( + oauth2.isValid(oneBlank, "scopes"), + "Dokumentiert aktuelles Verhalten: nicht-leere Liste gilt als gültig, auch wenn Element leer/blank ist"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java new file mode 100644 index 000000000..3fa8299ca --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2HttpTest.java @@ -0,0 +1,80 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; + +class ApplicationPropertiesSaml2HttpTest { + + @Test + void idpMetadataUri_http_is_resolved_via_mockwebserver() throws Exception { + try (MockWebServer server = new MockWebServer()) { + server.enqueue( + new MockResponse() + .setResponseCode(200) + .addHeader("Content-Type", "application/xml") + .setBody("")); + server.start(); + + String url = server.url("/meta").toString(); + + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri(url); + + try (InputStream in = s.getIdpMetadataUri()) { + String body = new String(in.readAllBytes(), StandardCharsets.UTF_8); + assertTrue(body.contains("EntityDescriptor")); + } + } + } + + @Test + void idpMetadataUri_invalidUri_triggers_catch_and_throwsIOException() { + // Ungültige URI -> new URI(...) wirft URISyntaxException -> catch -> IOException + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri("http:##invalid uri"); // absichtlich kaputt (Leerzeichen + ##) + + assertThrows(IOException.class, s::getIdpMetadataUri); + } + + @Test + void spCert_else_branch_returns_FileSystemResource_for_filesystem_path() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + + // temporäre Datei simuliert "Filesystem"-Pfad (-> else-Zweig) + Path tmp = Files.createTempFile("spdf-spcert-", ".crt"); + Files.writeString(tmp, "CERT"); + + s.setSpCert(tmp.toString()); + Resource r = s.getSpCert(); + + assertNotNull(r); + assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path"); + assertTrue(r.exists(), "Temp file should exist"); + } + + @Test + void idpCert_else_branch_returns_FileSystemResource_even_if_missing() { + var s = new ApplicationProperties.Security.SAML2(); + + // bewusst nicht existierender Pfad -> else-Zweig wird trotzdem genommen + String missing = "/this/path/does/not/exist/idp.crt"; + s.setIdpCert(missing); + Resource r = s.getIdpCert(); + + assertNotNull(r); + assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path"); + assertFalse(r.exists(), "Resource should not exist for missing file"); + } +} diff --git a/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java new file mode 100644 index 000000000..efc266561 --- /dev/null +++ b/app/common/src/test/java/stirling/software/common/model/ApplicationPropertiesSaml2ResourceTest.java @@ -0,0 +1,55 @@ +package stirling.software.common.model; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.Resource; + +class ApplicationPropertiesSaml2ResourceTest { + + @Test + void idpMetadataUri_classpath_is_resolved() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + s.setIdpMetadataUri("classpath:saml/dummy.txt"); + + try (InputStream in = s.getIdpMetadataUri()) { + assertNotNull(in, "Classpath InputStream should not be null"); + String txt = new String(in.readAllBytes(), StandardCharsets.UTF_8); + assertTrue(txt.contains("ok")); + } + } + + @Test + void spCert_idpCert_privateKey_null_classpath_and_filesystem() throws Exception { + var s = new ApplicationProperties.Security.SAML2(); + + s.setSpCert(null); + s.setIdpCert(null); + s.setPrivateKey(null); + assertNull(s.getSpCert()); + assertNull(s.getIdpCert()); + assertNull(s.getPrivateKey()); + + s.setSpCert("classpath:saml/dummy.txt"); + s.setIdpCert("classpath:saml/dummy.txt"); + s.setPrivateKey("classpath:saml/dummy.txt"); + Resource sp = s.getSpCert(); + Resource idp = s.getIdpCert(); + Resource pk = s.getPrivateKey(); + assertTrue(sp.exists()); + assertTrue(idp.exists()); + assertTrue(pk.exists()); + + Path tmp = Files.createTempFile("spdf-key-", ".pem"); + Files.writeString(tmp, "KEY"); + s.setPrivateKey(tmp.toString()); + Resource pkFs = s.getPrivateKey(); + assertNotNull(pkFs); + assertTrue(pkFs.exists()); + } +} diff --git a/app/common/src/test/resources/saml/dummy.txt b/app/common/src/test/resources/saml/dummy.txt new file mode 100644 index 000000000..9766475a4 --- /dev/null +++ b/app/common/src/test/resources/saml/dummy.txt @@ -0,0 +1 @@ +ok diff --git a/build.gradle b/build.gradle index e54c58e7d..2c151d11b 100644 --- a/build.gradle +++ b/build.gradle @@ -69,7 +69,7 @@ allprojects { tasks.register('writeVersion', WriteProperties) { outputFile = layout.projectDirectory.file('app/common/src/main/resources/version.properties') println "Writing version.properties to ${outputFile.path}" - comment "${new Date()}" + comment = "${new Date()}" property 'version', project.provider { project.version.toString() } } @@ -128,6 +128,9 @@ subprojects { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" + + testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0") + testImplementation "com.squareup.okhttp3:mockwebserver" } tasks.withType(JavaCompile).configureEach { @@ -153,6 +156,17 @@ subprojects { } } + jacocoTestCoverageVerification { + dependsOn jacocoTestReport + violationRules { + rule { + limit { + minimum = 0.0 + } + } + } + } + tasks.named("processResources") { dependsOn(rootProject.tasks.writeVersion) } @@ -569,6 +583,9 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly "org.junit.platform:junit-platform-launcher:$junitPlatformVersion" + + testImplementation platform("com.squareup.okhttp3:okhttp-bom:5.1.0") + testImplementation "com.squareup.okhttp3:mockwebserver" } tasks.named("test") { From dd0bf194cda86b3a783b04f6b07b6fe9906b935f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:31:28 +0200 Subject: [PATCH 22/23] Update Hungarian translation for new update related strings (#4152) # Description of Changes --- ## 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/devGuide/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/devGuide/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/devGuide/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/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../main/resources/messages_hu_HU.properties | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/app/core/src/main/resources/messages_hu_HU.properties b/app/core/src/main/resources/messages_hu_HU.properties index 45de2334c..c5488bc2b 100644 --- a/app/core/src/main/resources/messages_hu_HU.properties +++ b/app/core/src/main/resources/messages_hu_HU.properties @@ -368,36 +368,36 @@ settings.update=Frissítés elérhető settings.updateAvailable=A jelenlegi telepített verzió: {0}. Új verzió ({1}) érhető el. # Update modal and notification strings -update.urgentUpdateAvailable=🚨 Update Available -update.updateAvailable=Update Available -update.modalTitle=Update Available -update.current=Current -update.latest=Latest -update.latestStable=Latest Stable -update.priority=Priority -update.recommendedAction=Recommended Action -update.breakingChangesDetected=⚠️ Breaking Changes Detected -update.breakingChangesMessage=This update contains breaking changes. Please review the migration guides below. -update.migrationGuides=Migration Guides: -update.viewGuide=View Guide -update.loadingDetailedInfo=Loading detailed version information... -update.close=Close -update.viewAllReleases=View All Releases -update.downloadLatest=Download Latest -update.availableUpdates=Available Updates: -update.unableToLoadDetails=Unable to load detailed version information. -update.version=Version +update.urgentUpdateAvailable=🚨 Sürgős frissítés érhető el +update.updateAvailable=Frissítés érhető el +update.modalTitle=Frissítés érhető el +update.current=Jelenlegi verzió +update.latest=Legújabb verzió +update.latestStable=Legújabb stabil verzió +update.priority=Fontosság +update.recommendedAction=Ajánlott lépés +update.breakingChangesDetected=⚠️ Jelentős változások észlelve +update.breakingChangesMessage=Ez a frissítés jelentős változásokat tartalmaz. Kérjük, olvassa el az alábbi migrációs útmutatót. +update.migrationGuides=Migrációs útmutatók: +update.viewGuide=Útmutató megtekintése +update.loadingDetailedInfo=Részletes verzióinformációk betöltése folyamatban... +update.close=Bezárás +update.viewAllReleases=Összes kiadás megtekintése +update.downloadLatest=Legújabb verzió letöltése +update.availableUpdates=Elérhető frissítések: +update.unableToLoadDetails=Nem sikerült betölteni a részletes verzióinformációkat. +update.version=Verzió # Update priority levels -update.priority.urgent=URGENT -update.priority.normal=NORMAL -update.priority.minor=MINOR -update.priority.low=LOW +update.priority.urgent=SÜRGETŐ +update.priority.normal=NORMÁL +update.priority.minor=KISEBB +update.priority.low=ALACSONY # Breaking changes text -update.breakingChanges=Breaking Changes: -update.breakingChangesDefault=This version contains breaking changes -update.migrationGuide=Migration Guide +update.breakingChanges=Megszakító változások: +update.breakingChangesDefault=Ez a verzió megszakító változásokat tartalmaz +update.migrationGuide=Migrációs útmutató settings.appVersion=Alkalmazás verziója: settings.downloadOption.title=Letöltési beállítás (egyetlen fájl, nem tömörített letöltések esetén): settings.downloadOption.1=Megnyitás ugyanabban az ablakban From 979f30227736361294a3744ad95178d7115b01e7 Mon Sep 17 00:00:00 2001 From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:33:08 +0100 Subject: [PATCH 23/23] :globe_with_meridians: Sync Translations + Update README Progress Table (#4159) ### Description of Changes This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made: #### **1. Synchronization of Translation Files** - Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`. - Ensured consistency and synchronization across all supported language files. - Highlighted any missing or incomplete translations. #### **2. Update README.md** - Generated the translation progress table in `README.md`. - Added a summary of the current translation status for all supported languages. - Included up-to-date statistics on translation coverage. #### **Why these changes are necessary** - Keeps translation files aligned with the latest reference updates. - Ensures the documentation reflects the current translation progress. --- Auto-generated by [create-pull-request][1]. [1]: https://github.com/peter-evans/create-pull-request Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e212780a..0533376b8 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Stirling-PDF currently supports 40 languages! | German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) | | Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) | | Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) | -| Hungarian (Magyar) (hu_HU) | ![97%](https://geps.dev/progress/97) | +| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) | | Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) | | Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |